From fd789d28dbe3938ec5fad139f95b6b06e966de53 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 5 Sep 2022 14:00:41 +1000 Subject: [PATCH] Add confirmation field for incomplete purchase orders (#3615) * Add confirmation field for incomplete purchase orders * Add similar functionality for SalesOrder - Complete form is now context sensitive - Allow user to specify if order should be closed with incomplete lines * Update API version * JS linting * Updated unit tests --- InvenTree/InvenTree/api_version.py | 6 +- InvenTree/order/models.py | 8 +-- InvenTree/order/serializers.py | 57 +++++++++++++++- .../templates/order/sales_order_base.html | 14 ++-- InvenTree/order/test_api.py | 14 +++- InvenTree/templates/js/translated/forms.js | 9 ++- InvenTree/templates/js/translated/order.js | 65 +++++++++++++++++++ 7 files changed, 155 insertions(+), 18 deletions(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 73e9d135d4..670203e2a9 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,15 @@ # InvenTree API version -INVENTREE_API_VERSION = 73 +INVENTREE_API_VERSION = 74 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v74 -> 2022-08-28 : https://github.com/inventree/InvenTree/pull/3615 + - Add confirmation field for completing PurchaseOrder if the order has incomplete lines + - Add confirmation field for completing SalesOrder if the order has incomplete lines + v73 -> 2022-08-24 : https://github.com/inventree/InvenTree/pull/3605 - Add 'description' field to PartParameterTemplate model diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 87ce0b525a..d3d4922d46 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -702,7 +702,7 @@ class SalesOrder(Order): """Check if this order is "shipped" (all line items delivered).""" return self.lines.count() > 0 and all([line.is_completed() for line in self.lines.all()]) - def can_complete(self, raise_error=False): + def can_complete(self, raise_error=False, allow_incomplete_lines=False): """Test if this SalesOrder can be completed. Throws a ValidationError if cannot be completed. @@ -720,7 +720,7 @@ class SalesOrder(Order): elif self.pending_shipment_count > 0: raise ValidationError(_("Order cannot be completed as there are incomplete shipments")) - elif self.pending_line_count > 0: + elif not allow_incomplete_lines and self.pending_line_count > 0: raise ValidationError(_("Order cannot be completed as there are incomplete line items")) except ValidationError as e: @@ -732,9 +732,9 @@ class SalesOrder(Order): return True - def complete_order(self, user): + def complete_order(self, user, **kwargs): """Mark this order as "complete.""" - if not self.can_complete(): + if not self.can_complete(**kwargs): return False self.status = SalesOrderStatus.SHIPPED diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index e2ec2c8761..f161d42b5b 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -19,7 +19,7 @@ import stock.models import stock.serializers from common.settings import currency_code_mappings from company.serializers import CompanyBriefSerializer, SupplierPartSerializer -from InvenTree.helpers import extract_serial_numbers, normalize +from InvenTree.helpers import extract_serial_numbers, normalize, str2bool from InvenTree.serializers import (InvenTreeAttachmentSerializer, InvenTreeDecimalField, InvenTreeModelSerializer, @@ -204,6 +204,23 @@ class PurchaseOrderCancelSerializer(serializers.Serializer): class PurchaseOrderCompleteSerializer(serializers.Serializer): """Serializer for completing a purchase order.""" + accept_incomplete = serializers.BooleanField( + label=_('Accept Incomplete'), + help_text=_('Allow order to be closed with incomplete line items'), + required=False, + default=False, + ) + + def validate_accept_incomplete(self, value): + """Check if the 'accept_incomplete' field is required""" + + order = self.context['order'] + + if not value and not order.is_complete: + raise ValidationError(_("Order has incomplete line items")) + + return value + class Meta: """Metaclass options.""" @@ -1079,13 +1096,43 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer): class SalesOrderCompleteSerializer(serializers.Serializer): """DRF serializer for manually marking a sales order as complete.""" + accept_incomplete = serializers.BooleanField( + label=_('Accept Incomplete'), + help_text=_('Allow order to be closed with incomplete line items'), + required=False, + default=False, + ) + + def validate_accept_incomplete(self, value): + """Check if the 'accept_incomplete' field is required""" + + order = self.context['order'] + + if not value and not order.is_completed(): + raise ValidationError(_("Order has incomplete line items")) + + return value + + def get_context_data(self): + """Custom context data for this serializer""" + + order = self.context['order'] + + return { + 'is_complete': order.is_completed(), + 'pending_shipments': order.pending_shipment_count, + } + def validate(self, data): """Custom validation for the serializer""" data = super().validate(data) order = self.context['order'] - order.can_complete(raise_error=True) + order.can_complete( + raise_error=True, + allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)), + ) return data @@ -1093,10 +1140,14 @@ class SalesOrderCompleteSerializer(serializers.Serializer): """Save the serializer to complete the SalesOrder""" request = self.context['request'] order = self.context['order'] + data = self.validated_data user = getattr(request, 'user', None) - order.complete_order(user) + order.complete_order( + user, + allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)), + ) class SalesOrderCancelSerializer(serializers.Serializer): diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 0c14d9859d..cb7f998578 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -64,7 +64,7 @@ src="{% static 'img/blank_image.png' %}" {% if order.status == SalesOrderStatus.PENDING %} - {% endif %} @@ -253,12 +253,12 @@ $("#cancel-order").click(function() { }); $("#complete-order").click(function() { - constructForm('{% url "api-so-complete" order.id %}', { - method: 'POST', - title: '{% trans "Complete Sales Order" %}', - confirm: true, - reload: true, - }); + completeSalesOrder( + {{ order.pk }}, + { + reload: true, + } + ); }); {% if report_enabled %} diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 3e5f63c72a..da8011923b 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -322,7 +322,19 @@ class PurchaseOrderTest(OrderTest): self.assignRole('purchase_order.add') - self.post(url, {}, expected_code=201) + # Should fail due to incomplete lines + response = self.post(url, {}, expected_code=400) + + self.assertIn('Order has incomplete line items', str(response.data['accept_incomplete'])) + + # Post again, accepting incomplete line items + self.post( + url, + { + 'accept_incomplete': True, + }, + expected_code=201 + ) po.refresh_from_db() diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index c3669e5cd4..093a471cca 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -328,8 +328,6 @@ function constructForm(url, options) { constructFormBody({}, options); } - options.fields = options.fields || {}; - // Save the URL options.url = url; @@ -351,6 +349,13 @@ function constructForm(url, options) { // Extract any custom 'context' information from the OPTIONS data options.context = OPTIONS.context || {}; + // Construct fields (can be a static parameter or a function) + if (options.fieldsFunction) { + options.fields = options.fieldsFunction(options); + } else { + options.fields = options.fields || {}; + } + /* * Determine what "type" of form we want to construct, * based on the requested action. diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 11641e426f..b9cd3b5a65 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -24,6 +24,7 @@ cancelPurchaseOrder, cancelSalesOrder, completePurchaseOrder, + completeSalesOrder, completeShipment, completePendingShipments, createPurchaseOrder, @@ -282,6 +283,17 @@ function completePurchaseOrder(order_id, options={}) { method: 'POST', title: '{% trans "Complete Purchase Order" %}', confirm: true, + fieldsFunction: function(opts) { + var fields = { + accept_incomplete: {}, + }; + + if (opts.context.is_complete) { + delete fields['accept_incomplete']; + } + + return fields; + }, preFormContent: function(opts) { var html = ` @@ -373,6 +385,59 @@ function issuePurchaseOrder(order_id, options={}) { } +/* + * Launches a modal form to mark a SalesOrder as "complete" + */ +function completeSalesOrder(order_id, options={}) { + + constructForm( + `/api/order/so/${order_id}/complete/`, + { + method: 'POST', + title: '{% trans "Complete Sales Order" %}', + confirm: true, + fieldsFunction: function(opts) { + var fields = { + accept_incomplete: {}, + }; + + if (opts.context.is_complete) { + delete fields['accept_incomplete']; + } + + return fields; + }, + preFormContent: function(opts) { + var html = ` +
+ {% trans "Mark this order as complete?" %} +
`; + + if (opts.context.pending_shipments) { + html += ` +
+ {% trans "Order cannot be completed as there are incomplete shipments" %}
+
`; + } + + if (!opts.context.is_complete) { + html += ` +
+ {% trans "This order has line items which have not been completed." %}
+ {% trans "Completing this order means that the order and line items will no longer be editable." %} +
`; + } + + return html; + }, + onSuccess: function(response) { + handleFormSuccess(response, options); + } + } + ); +} + + /* * Launches a modal form to mark a SalesOrder as "cancelled" */