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
This commit is contained in:
Oliver 2022-09-05 14:00:41 +10:00 committed by GitHub
parent b7d0bb9820
commit fd789d28db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 155 additions and 18 deletions

View File

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

View File

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

View File

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

View File

@ -64,7 +64,7 @@ src="{% static 'img/blank_image.png' %}"
</div>
{% if order.status == SalesOrderStatus.PENDING %}
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Complete Sales Order" %}'{% if not order.is_completed %} disabled{% endif %}>
<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>
{% 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 %}

View File

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

View File

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

View File

@ -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 = `
<div class='alert alert-block alert-info'>
{% 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) {
handleFormSuccess(response, options);
}
}
);
}
/*
* Launches a modal form to mark a SalesOrder as "cancelled"
*/