Repository: saleor/saleor

Issue:
Add query to fetch draft orders
In the Dashboard 2.0 we want to have separate links to view draft orders and confirmed ones. For that reason we need to add a separate resolver to return only draft orders. Exiting `orders` resolver should be changed to return confirmed orders.


Discussion:
How about adding ```orderStatus``` or ```orderStatuses``` argument which can filter the draft order or orders with any statuses in the backend for orders API? This can unify the API, for example if we need to filter the canceled orders later, we only need to pass ```[CANCEL]``` to ```orderStatuses``` without creating a new API.
I agree with @jxltom, a new resolver makes no sense if we have means to use filtering. We already have such argument, but it only allows filtering on two statuses:
```
class OrderStatusFilter(graphene.Enum):
    READY_TO_FULFILL = 'READY_TO_FULFILL'
    READY_TO_CAPTURE = 'READY_TO_CAPTURE'
```
It could be improved to actually use all the status choices and perhaps accept a list of status choices instead of just one.

I agree that we need to extend filtering capabilities in the `orders` resolver - that will be required by Dashboard 2.0 anyway at some point. But I would exclude draft status from this resolver and have a separate one for a few reasons:
- drafts use different logic than regular orders - they can be edited in many ways or deleted from DB,
- by having a separate field in API we make it clear to API consumers that "drafts" and "regular" orders are a bit different concepts,
- draft orders are visible only to staff users - it's easier to require permission when we have a separate field for that.


Resolving patch:
diff --git a/saleor/graphql/order/resolvers.py b/saleor/graphql/order/resolvers.py
--- a/saleor/graphql/order/resolvers.py
+++ b/saleor/graphql/order/resolvers.py
@@ -12,12 +12,7 @@
     'id', 'discount_name', 'token', 'user_email', 'user__email')
 
 
-def resolve_orders(info, created, status, query):
-    user = info.context.user
-    if user.has_perm('order.manage_orders'):
-        qs = models.Order.objects.all()
-    else:
-        qs = user.orders.confirmed()
+def filter_orders(qs, info, created, status, query):
     qs = filter_by_query_param(qs, query, ORDER_SEARCH_FIELDS)
 
     # filter orders by status
@@ -34,6 +29,20 @@ def resolve_orders(info, created, status, query):
     return gql_optimizer.query(qs, info)
 
 
+def resolve_orders(info, created, status, query):
+    user = info.context.user
+    if user.has_perm('order.manage_orders'):
+        qs = models.Order.objects.confirmed()
+    else:
+        qs = user.orders.confirmed()
+    return filter_orders(qs, info, created, status, query)
+
+
+def resolve_draft_orders(info, created, query):
+    qs = models.Order.objects.drafts()
+    return filter_orders(qs, info, created, None, query)
+
+
 def resolve_orders_total(info, period):
     qs = models.Order.objects.confirmed().exclude(status=OrderStatus.CANCELED)
     qs = filter_by_period(qs, period, 'created')
diff --git a/saleor/graphql/order/schema.py b/saleor/graphql/order/schema.py
--- a/saleor/graphql/order/schema.py
+++ b/saleor/graphql/order/schema.py
@@ -19,8 +19,8 @@
     OrderAddNote, OrderCancel, OrderCapture, OrderMarkAsPaid, OrderRefund,
     OrderUpdate, OrderUpdateShipping, OrderVoid)
 from .resolvers import (
-    resolve_homepage_events, resolve_order, resolve_order_by_token,
-    resolve_orders, resolve_orders_total)
+    resolve_draft_orders, resolve_homepage_events, resolve_order,
+    resolve_order_by_token, resolve_orders, resolve_orders_total)
 from .types import Order, OrderEvent
 
 
@@ -40,6 +40,13 @@ class OrderQueries(graphene.ObjectType):
         status=graphene.Argument(
             OrderStatusFilter, description='Filter order by status'),
         description='List of the shop\'s orders.')
+    draft_orders = PrefetchingConnectionField(
+        Order,
+        query=graphene.String(description=DESCRIPTIONS['order']),
+        created=graphene.Argument(
+            ReportingPeriod,
+            description='Filter draft orders from a selected timespan.'),
+        description='List of the shop\'s draft orders.')
     orders_total = graphene.Field(
         TaxedMoney, description='Total sales.',
         period=graphene.Argument(
@@ -62,6 +69,10 @@ def resolve_orders(
             self, info, created=None, status=None, query=None, **kwargs):
         return resolve_orders(info, created, status, query)
 
+    @permission_required('order.manage_orders')
+    def resolve_draft_orders(self, info, created=None, query=None, **kwargs):
+        return resolve_draft_orders(info, created, query)
+
     @permission_required('order.manage_orders')
     def resolve_orders_total(self, info, period, **kwargs):
         return resolve_orders_total(info, period)


Test patch:
diff --git a/tests/api/test_order.py b/tests/api/test_order.py
--- a/tests/api/test_order.py
+++ b/tests/api/test_order.py
@@ -1,3 +1,4 @@
+import uuid
 from unittest.mock import MagicMock, Mock, patch
 
 import graphene
@@ -20,6 +21,31 @@
 from .utils import assert_no_permission, get_graphql_content
 
 
+@pytest.fixture
+def orders(customer_user):
+    Order.objects.bulk_create([
+        Order(
+            user=customer_user,
+            status=OrderStatus.CANCELED,
+            token=uuid.uuid4()),
+        Order(
+            user=customer_user,
+            status=OrderStatus.UNFULFILLED,
+            token=uuid.uuid4()),
+        Order(
+            user=customer_user,
+            status=OrderStatus.PARTIALLY_FULFILLED,
+            token=uuid.uuid4()),
+        Order(
+            user=customer_user,
+            status=OrderStatus.FULFILLED,
+            token=uuid.uuid4()),
+        Order(
+            user=customer_user,
+            status=OrderStatus.DRAFT,
+            token=uuid.uuid4())])
+
+
 def test_orderline_query(
         staff_api_client, permission_manage_orders, fulfilled_order):
     order = fulfilled_order
@@ -148,24 +174,46 @@ def test_order_query(
     assert expected_method.type.upper() == method['type']
 
 
-def test_order_status_filter_param(user_api_client):
+def test_order_query_draft_excluded(
+        staff_api_client, permission_manage_orders, orders):
     query = """
-    query OrdersQuery($status: OrderStatusFilter) {
-        orders(status: $status) {
-            totalCount
+    query OrdersQuery {
+        orders(first: 10) {
+            edges {
+                node {
+                    id
+                }
+            }
         }
     }
     """
 
-    # Check that both calls return a succesful response (underlying logic
-    # is tested separately in querysets' tests).
-    variables = {'status': OrderStatusFilter.READY_TO_CAPTURE.name}
-    response = user_api_client.post_graphql(query, variables)
-    get_graphql_content(response)
+    staff_api_client.user.user_permissions.add(permission_manage_orders)
+    response = staff_api_client.post_graphql(query)
+    edges = get_graphql_content(response)['data']['orders']['edges']
 
-    variables = {'status': OrderStatusFilter.READY_TO_FULFILL.name}
-    response = user_api_client.post_graphql(query, variables)
-    get_graphql_content(response)
+    assert len(edges) == Order.objects.confirmed().count()
+
+
+def test_draft_order_query(
+        staff_api_client, permission_manage_orders, orders):
+    query = """
+    query DraftOrdersQuery {
+        draftOrders(first: 10) {
+            edges {
+                node {
+                    id
+                }
+            }
+        }
+    }
+    """
+
+    staff_api_client.user.user_permissions.add(permission_manage_orders)
+    response = staff_api_client.post_graphql(query)
+    edges = get_graphql_content(response)['data']['draftOrders']['edges']
+
+    assert len(edges) == Order.objects.drafts().count()
 
 
 def test_nested_order_events_query(