diff --git a/src/backend/InvenTree/InvenTree/status_codes.py b/src/backend/InvenTree/InvenTree/status_codes.py index 8d6aee3562..2cd3f110ba 100644 --- a/src/backend/InvenTree/InvenTree/status_codes.py +++ b/src/backend/InvenTree/InvenTree/status_codes.py @@ -41,6 +41,7 @@ class SalesOrderStatus(StatusCode): 'primary', ) # Order has been issued, and is in progress SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer + COMPLETE = 30, _('Complete'), 'success' # Order is complete CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled LOST = 50, _('Lost'), 'warning' # Order was lost RETURNED = 60, _('Returned'), 'warning' # Order was returned @@ -53,7 +54,7 @@ class SalesOrderStatusGroups: OPEN = [SalesOrderStatus.PENDING.value, SalesOrderStatus.IN_PROGRESS.value] # Completed orders - COMPLETE = [SalesOrderStatus.SHIPPED.value] + COMPLETE = [SalesOrderStatus.SHIPPED.value, SalesOrderStatus.COMPLETE.value] class StockStatus(StatusCode): diff --git a/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py b/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py new file mode 100644 index 0000000000..de56926bc9 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0099_alter_salesorder_status.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.11 on 2024-04-03 00:40 + +import django.core.validators +from django.db import migrations, models +import InvenTree.status_codes + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0098_auto_20231024_1844'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorder', + name='status', + field=models.PositiveIntegerField(choices=InvenTree.status_codes.SalesOrderStatus.items(), default=10, help_text='Purchase order status', verbose_name='Status'), + ), + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 94710c01a9..8eae8562ed 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1000,7 +1000,7 @@ class SalesOrder(TotalPriceMixin, Order): ) # Only an open order can be marked as shipped - elif not self.is_open: + elif not self.is_open and not self.is_completed: raise ValidationError(_('Only an open order can be marked as complete')) elif self.pending_shipment_count > 0: @@ -1042,9 +1042,12 @@ class SalesOrder(TotalPriceMixin, Order): if not self.can_complete(**kwargs): return False - self.status = SalesOrderStatus.SHIPPED.value - self.shipped_by = user - self.shipment_date = InvenTree.helpers.current_date() + if self.status == SalesOrderStatus.SHIPPED: + self.status = SalesOrderStatus.COMPLETE.value + else: + self.status = SalesOrderStatus.SHIPPED.value + self.shipped_by = user + self.shipment_date = InvenTree.helpers.current_date() self.save() @@ -1098,7 +1101,7 @@ class SalesOrder(TotalPriceMixin, Order): ) @transaction.atomic - def complete_order(self, user, **kwargs): + def ship_order(self, user, **kwargs): """Attempt to transition to SHIPPED status.""" return self.handle_transition( self.status, @@ -1109,6 +1112,18 @@ class SalesOrder(TotalPriceMixin, Order): **kwargs, ) + @transaction.atomic + def complete_order(self, user, **kwargs): + """Attempt to transition to COMPLETED status.""" + return self.handle_transition( + self.status, + SalesOrderStatus.COMPLETED.value, + self, + self._action_complete, + user=user, + **kwargs, + ) + @transaction.atomic def cancel_order(self): """Attempt to transition to CANCELLED status.""" diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index a1da09f21b..12f8e3b86d 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -1288,7 +1288,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer): user = getattr(request, 'user', None) - order.complete_order( + order.ship_order( user, allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)) ) diff --git a/src/backend/InvenTree/order/templates/order/sales_order_base.html b/src/backend/InvenTree/order/templates/order/sales_order_base.html index f184d8d2bc..e1ed302804 100644 --- a/src/backend/InvenTree/order/templates/order/sales_order_base.html +++ b/src/backend/InvenTree/order/templates/order/sales_order_base.html @@ -88,7 +88,11 @@ src="{% static 'img/blank_image.png' %}" + {% endif %} + {% elif order.status == SalesOrderStatus.SHIPPED %} @@ -286,6 +290,15 @@ $("#cancel-order").click(function() { ); }); +$("#ship-order").click(function() { + shipSalesOrder( + {{ order.pk }}, + { + reload: true, + } + ); +}); + $("#complete-order").click(function() { completeSalesOrder( {{ order.pk }}, diff --git a/src/backend/InvenTree/order/test_sales_order.py b/src/backend/InvenTree/order/test_sales_order.py index da8bcecb0e..b394147971 100644 --- a/src/backend/InvenTree/order/test_sales_order.py +++ b/src/backend/InvenTree/order/test_sales_order.py @@ -211,7 +211,7 @@ class SalesOrderTest(TestCase): self.order.can_complete(raise_error=True) # Now try to ship it - should fail - result = self.order.complete_order(None) + result = self.order.ship_order(None) self.assertFalse(result) def test_complete_order(self): @@ -225,8 +225,8 @@ class SalesOrderTest(TestCase): self.assertEqual(SalesOrderAllocation.objects.count(), 2) - # Attempt to complete the order (but shipments are not completed!) - result = self.order.complete_order(None) + # Attempt to ship the order (but shipments are not completed!) + result = self.order.ship_order(None) self.assertFalse(result) @@ -238,7 +238,7 @@ class SalesOrderTest(TestCase): self.assertTrue(self.shipment.is_complete()) # Now, should be OK to ship - result = self.order.complete_order(None) + result = self.order.ship_order(None) self.assertTrue(result) diff --git a/src/backend/InvenTree/templates/js/translated/sales_order.js b/src/backend/InvenTree/templates/js/translated/sales_order.js index 53085cf180..f7c6eea8a3 100644 --- a/src/backend/InvenTree/templates/js/translated/sales_order.js +++ b/src/backend/InvenTree/templates/js/translated/sales_order.js @@ -473,7 +473,59 @@ function completePendingShipmentsHelper(shipments, shipment_idx, options={}) { /* - * Launches a modal form to mark a SalesOrder as "complete" + * Launches a modal form to mark a SalesOrder as "shipped" + */ +function shipSalesOrder(order_id, options={}) { + + constructForm( + `/api/order/so/${order_id}/complete/`, + { + method: 'POST', + title: '{% trans "Ship 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 "Ship this order?" %} +
`; + + if (opts.context.pending_shipments) { + html += ` +
+ {% trans "Order cannot be shipped as there are incomplete shipments" %}
+
`; + } + + if (!opts.context.is_complete) { + html += ` +
+ {% trans "This order has line items which have not been completed." %}
+ {% trans "Shipping 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 "completed" */ function completeSalesOrder(order_id, options={}) { @@ -500,21 +552,6 @@ function completeSalesOrder(order_id, options={}) { {% 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) {