Add support for a Completed status (Fixes: #6434) (#6449)

* Add support for a Completed status (Fixes: #6434)

* Remove unnecessary validator

* Update sales order tests for the new model functions
This commit is contained in:
Simon Quigley 2024-05-15 21:30:29 -05:00 committed by GitHub
parent b7b320cf61
commit e8f8f3b3ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 113 additions and 27 deletions

View File

@ -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):

View File

@ -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'),
),
]

View File

@ -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."""

View File

@ -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))
)

View File

@ -88,7 +88,11 @@ src="{% static 'img/blank_image.png' %}"
<button type='button' class='btn btn-success' id='complete-order-shipments' title='{% trans "Ship Items" %}'>
<span class='fas fa-truck'></span> {% trans "Ship Items" %}
</button>
<button type='button' class='btn btn-success' id='ship-order' title='{% trans "Mark As Shipped" %}'>
<span class='fas fa-check-circle'></span> {% trans "Mark As Shipped" %}
</button>
{% endif %}
{% elif order.status == SalesOrderStatus.SHIPPED %}
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Complete Sales Order" %}'>
<span class='fas fa-check-circle'></span> {% trans "Complete Order" %}
</button>
@ -286,6 +290,15 @@ $("#cancel-order").click(function() {
);
});
$("#ship-order").click(function() {
shipSalesOrder(
{{ order.pk }},
{
reload: true,
}
);
});
$("#complete-order").click(function() {
completeSalesOrder(
{{ order.pk }},

View File

@ -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)

View File

@ -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 = `
<div class='alert alert-block alert-info'>
{% trans "Ship this order?" %}
</div>`;
if (opts.context.pending_shipments) {
html += `
<div class='alert alert-block alert-danger'>
{% trans "Order cannot be shipped as there are incomplete shipments" %}<br>
</div>`;
}
if (!opts.context.is_complete) {
html += `
<div class='alert alert-block alert-warning'>
{% trans "This order has line items which have not been completed." %}<br>
{% trans "Shipping this order means that the order and line items will no longer be editable." %}
</div>`;
}
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?" %}
</div>`;
if (opts.context.pending_shipments) {
html += `
<div class='alert alert-block alert-danger'>
{% trans "Order cannot be completed as there are incomplete shipments" %}<br>
</div>`;
}
if (!opts.context.is_complete) {
html += `
<div class='alert alert-block alert-warning'>
{% trans "This order has line items which have not been completed." %}<br>
{% trans "Completing this order means that the order and line items will no longer be editable." %}
</div>`;
}
return html;
},
onSuccess: function(response) {