Repository: saleor/saleor

Issue:
Return `canFinalize` flag in draft order
### What I'm trying to achieve
To get an information whether a draft order can be finalized or not. This way we can avoid duplicating logic in the dashboard.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/order/mutations/draft_orders.py b/saleor/graphql/order/mutations/draft_orders.py
--- a/saleor/graphql/order/mutations/draft_orders.py
+++ b/saleor/graphql/order/mutations/draft_orders.py
@@ -13,9 +13,10 @@
 from ...account.i18n import I18nMixin
 from ...account.types import AddressInput
 from ...core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
-from ...core.types.common import Decimal, Error
+from ...core.types.common import Decimal
 from ...product.types import ProductVariant
 from ..types import Order, OrderLine
+from ..utils import can_finalize_draft_order
 
 
 class OrderLineInput(graphene.InputObjectType):
@@ -153,32 +154,6 @@ def user_is_allowed(cls, user, input):
         return user.has_perm('order.manage_orders')
 
 
-def check_for_draft_order_errors(order, errors):
-    """Return a list of errors associated with the order.
-
-    Checks, if given order has a proper shipping address and method
-    set up and return list of errors if not.
-    """
-    if order.get_total_quantity() == 0:
-        errors.append(
-            Error(
-                field='lines',
-                message='Could not create order without any products.'))
-    if order.is_shipping_required():
-        method = order.shipping_method
-        shipping_address = order.shipping_address
-        shipping_not_valid = (
-            method and shipping_address and
-            shipping_address.country.code not in method.shipping_zone.countries)  # noqa
-        if shipping_not_valid:
-            errors.append(
-                Error(
-                    field='shipping',
-                    message='Shipping method is not valid for chosen shipping '
-                            'address'))
-    return errors
-
-
 class DraftOrderComplete(BaseMutation):
     order = graphene.Field(Order, description='Completed order.')
 
@@ -191,7 +166,7 @@ class Meta:
         description = 'Completes creating an order.'
 
     @classmethod
-    def update_user_fields(cls, order, errors):
+    def update_user_fields(cls, order):
         if order.user:
             order.user_email = order.user.email
         elif order.user_email:
@@ -199,21 +174,18 @@ def update_user_fields(cls, order, errors):
                 order.user = User.objects.get(email=order.user_email)
             except User.DoesNotExist:
                 order.user = None
-        else:
-            cls.add_error(
-                errors, field=None,
-                message='Both user and user_email fields are null')
 
     @classmethod
     @permission_required('order.manage_orders')
     def mutate(cls, root, info, id):
         errors = []
         order = cls.get_node_or_error(info, id, errors, 'id', Order)
-        errors = check_for_draft_order_errors(order, errors)
-        cls.update_user_fields(order, errors)
+        if order:
+            errors = can_finalize_draft_order(order, errors)
         if errors:
             return cls(errors=errors)
 
+        cls.update_user_fields(order)
         order.status = OrderStatus.UNFULFILLED
         if not order.is_shipping_required():
             order.shipping_method_name = None
diff --git a/saleor/graphql/order/types.py b/saleor/graphql/order/types.py
--- a/saleor/graphql/order/types.py
+++ b/saleor/graphql/order/types.py
@@ -9,6 +9,7 @@
 from ..core.types.money import Money, TaxedMoney
 from ..payment.types import OrderAction, Payment, PaymentChargeStatusEnum
 from ..shipping.types import ShippingMethod
+from .utils import can_finalize_draft_order
 
 OrderEventsEnum = graphene.Enum.from_enum(OrderEvents)
 OrderEventsEmailsEnum = graphene.Enum.from_enum(OrderEventsEmails)
@@ -175,6 +176,10 @@ class Order(CountableDjangoObjectType):
         TaxedMoney,
         description='The sum of line prices not including shipping.')
     status_display = graphene.String(description='User-friendly order status.')
+    can_finalize = graphene.Boolean(
+        description=(
+            'Informs whether a draft order can be finalized',
+            '(turned into a regular order).'))
     total_authorized = graphene.Field(
         Money, description='Amount authorized for the order.')
     total_captured = graphene.Field(
@@ -284,6 +289,11 @@ def resolve_payments(self, info):
     def resolve_status_display(self, info):
         return self.get_status_display()
 
+    @staticmethod
+    def resolve_can_finalize(self, info):
+        errors = can_finalize_draft_order(self, [])
+        return not errors
+
     @staticmethod
     def resolve_user_email(self, info):
         if self.user_email:
diff --git a/saleor/graphql/order/utils.py b/saleor/graphql/order/utils.py
new file mode 100644
--- /dev/null
+++ b/saleor/graphql/order/utils.py
@@ -0,0 +1,32 @@
+from ..core.types.common import Error
+
+
+def can_finalize_draft_order(order, errors):
+    """Return a list of errors associated with the order.
+
+    Checks, if given order has a proper customer data, shipping
+    address and method set up and return list of errors if not.
+    """
+    if order.get_total_quantity() == 0:
+        errors.append(
+            Error(
+                field='lines',
+                message='Could not create order without any products.'))
+    if order.is_shipping_required():
+        method = order.shipping_method
+        shipping_address = order.shipping_address
+        shipping_not_valid = (
+            method and shipping_address and
+            shipping_address.country.code not in method.shipping_zone.countries)  # noqa
+        if shipping_not_valid:
+            errors.append(
+                Error(
+                    field='shipping',
+                    message='Shipping method is not valid for chosen shipping '
+                            'address'))
+    if not order.user and not order.user_email:
+        errors.append(
+            Error(
+                field=None,
+                message='Both user and user_email fields are null'))
+    return errors


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
@@ -4,15 +4,13 @@
 import pytest
 from tests.api.utils import get_graphql_content
 
-from saleor.account.models import Address
 from saleor.core.utils.taxes import ZERO_TAXED_MONEY
 from saleor.graphql.core.types import ReportingPeriod
-from saleor.graphql.order.mutations.draft_orders import (
-    check_for_draft_order_errors)
 from saleor.graphql.order.mutations.orders import (
     clean_order_cancel, clean_order_capture, clean_order_mark_as_paid,
     clean_refund_payment, clean_void_payment)
 from saleor.graphql.order.types import OrderEventsEmailsEnum, OrderStatusFilter
+from saleor.graphql.order.utils import can_finalize_draft_order
 from saleor.graphql.payment.types import PaymentChargeStatusEnum
 from saleor.order import OrderEvents, OrderEventsEmails, OrderStatus
 from saleor.order.models import Order, OrderEvent
@@ -62,6 +60,7 @@ def test_order_query(
             edges {
                 node {
                     number
+                    canFinalize
                     status
                     statusDisplay
                     paymentStatus
@@ -113,6 +112,7 @@ def test_order_query(
     content = get_graphql_content(response)
     order_data = content['data']['orders']['edges'][0]['node']
     assert order_data['number'] == str(order.pk)
+    assert order_data['canFinalize'] is True
     assert order_data['status'] == order.status.upper()
     assert order_data['statusDisplay'] == order.get_status_display()
     assert order_data['paymentStatus'] == order.get_last_payment_status()
@@ -355,24 +355,54 @@ def test_draft_order_delete(
         order.refresh_from_db()
 
 
-def test_check_for_draft_order_errors(order_with_lines):
-    errors = check_for_draft_order_errors(order_with_lines, [])
+ORDER_CAN_FINALIZE_QUERY = """
+    query OrderQuery($id: ID!){
+        order(id: $id){
+            canFinalize
+        }
+    }
+"""
+
+def test_can_finalize_order(
+        staff_api_client, permission_manage_orders, order_with_lines):
+    order_id = graphene.Node.to_global_id('Order', order_with_lines.id)
+    variables = {'id': order_id}
+    staff_api_client.user.user_permissions.add(permission_manage_orders)
+    response = staff_api_client.post_graphql(
+        ORDER_CAN_FINALIZE_QUERY, variables)
+    content = get_graphql_content(response)
+    assert content['data']['order']['canFinalize'] is True
+
+
+def test_can_finalize_order_no_order_lines(
+        staff_api_client, permission_manage_orders, order):
+    order_id = graphene.Node.to_global_id('Order', order.id)
+    variables = {'id': order_id}
+    staff_api_client.user.user_permissions.add(permission_manage_orders)
+    response = staff_api_client.post_graphql(
+        ORDER_CAN_FINALIZE_QUERY, variables)
+    content = get_graphql_content(response)
+    assert content['data']['order']['canFinalize'] is False
+
+
+def test_can_finalize_draft_order(order_with_lines):
+    errors = can_finalize_draft_order(order_with_lines, [])
     assert not errors
 
 
-def test_check_for_draft_order_errors_wrong_shipping(order_with_lines):
+def test_can_finalize_draft_order_wrong_shipping(order_with_lines):
     order = order_with_lines
     shipping_zone = order.shipping_method.shipping_zone
     shipping_zone.countries = ['DE']
     shipping_zone.save()
     assert order.shipping_address.country.code not in shipping_zone.countries
-    errors = check_for_draft_order_errors(order, [])
+    errors = can_finalize_draft_order(order, [])
     msg = 'Shipping method is not valid for chosen shipping address'
     assert errors[0].message == msg
 
 
-def test_check_for_draft_order_errors_no_order_lines(order):
-    errors = check_for_draft_order_errors(order, [])
+def test_can_finalize_draft_order_no_order_lines(order):
+    errors = can_finalize_draft_order(order, [])
     assert errors[0].message == 'Could not create order without any products.'