mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 20:46:47 +00:00
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:
parent
b7d0bb9820
commit
fd789d28db
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# 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
|
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
|
v73 -> 2022-08-24 : https://github.com/inventree/InvenTree/pull/3605
|
||||||
- Add 'description' field to PartParameterTemplate model
|
- Add 'description' field to PartParameterTemplate model
|
||||||
|
|
||||||
|
@ -702,7 +702,7 @@ class SalesOrder(Order):
|
|||||||
"""Check if this order is "shipped" (all line items delivered)."""
|
"""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()])
|
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.
|
"""Test if this SalesOrder can be completed.
|
||||||
|
|
||||||
Throws a ValidationError if cannot be completed.
|
Throws a ValidationError if cannot be completed.
|
||||||
@ -720,7 +720,7 @@ class SalesOrder(Order):
|
|||||||
elif self.pending_shipment_count > 0:
|
elif self.pending_shipment_count > 0:
|
||||||
raise ValidationError(_("Order cannot be completed as there are incomplete shipments"))
|
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"))
|
raise ValidationError(_("Order cannot be completed as there are incomplete line items"))
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@ -732,9 +732,9 @@ class SalesOrder(Order):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def complete_order(self, user):
|
def complete_order(self, user, **kwargs):
|
||||||
"""Mark this order as "complete."""
|
"""Mark this order as "complete."""
|
||||||
if not self.can_complete():
|
if not self.can_complete(**kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.status = SalesOrderStatus.SHIPPED
|
self.status = SalesOrderStatus.SHIPPED
|
||||||
|
@ -19,7 +19,7 @@ import stock.models
|
|||||||
import stock.serializers
|
import stock.serializers
|
||||||
from common.settings import currency_code_mappings
|
from common.settings import currency_code_mappings
|
||||||
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
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,
|
from InvenTree.serializers import (InvenTreeAttachmentSerializer,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
@ -204,6 +204,23 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
|||||||
class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||||
"""Serializer for completing a purchase order."""
|
"""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:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
@ -1079,13 +1096,43 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
|||||||
class SalesOrderCompleteSerializer(serializers.Serializer):
|
class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||||
"""DRF serializer for manually marking a sales order as complete."""
|
"""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):
|
def validate(self, data):
|
||||||
"""Custom validation for the serializer"""
|
"""Custom validation for the serializer"""
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|
||||||
order = self.context['order']
|
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
|
return data
|
||||||
|
|
||||||
@ -1093,10 +1140,14 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
|||||||
"""Save the serializer to complete the SalesOrder"""
|
"""Save the serializer to complete the SalesOrder"""
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
order = self.context['order']
|
order = self.context['order']
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
user = getattr(request, 'user', None)
|
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):
|
class SalesOrderCancelSerializer(serializers.Serializer):
|
||||||
|
@ -64,7 +64,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% 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" %}
|
<span class='fas fa-check-circle'></span> {% trans "Complete Order" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -253,12 +253,12 @@ $("#cancel-order").click(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#complete-order").click(function() {
|
$("#complete-order").click(function() {
|
||||||
constructForm('{% url "api-so-complete" order.id %}', {
|
completeSalesOrder(
|
||||||
method: 'POST',
|
{{ order.pk }},
|
||||||
title: '{% trans "Complete Sales Order" %}',
|
{
|
||||||
confirm: true,
|
reload: true,
|
||||||
reload: true,
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if report_enabled %}
|
{% if report_enabled %}
|
||||||
|
@ -322,7 +322,19 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
|
|
||||||
self.assignRole('purchase_order.add')
|
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()
|
po.refresh_from_db()
|
||||||
|
|
||||||
|
@ -328,8 +328,6 @@ function constructForm(url, options) {
|
|||||||
constructFormBody({}, options);
|
constructFormBody({}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
options.fields = options.fields || {};
|
|
||||||
|
|
||||||
// Save the URL
|
// Save the URL
|
||||||
options.url = url;
|
options.url = url;
|
||||||
|
|
||||||
@ -351,6 +349,13 @@ function constructForm(url, options) {
|
|||||||
// Extract any custom 'context' information from the OPTIONS data
|
// Extract any custom 'context' information from the OPTIONS data
|
||||||
options.context = OPTIONS.context || {};
|
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,
|
* Determine what "type" of form we want to construct,
|
||||||
* based on the requested action.
|
* based on the requested action.
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
cancelPurchaseOrder,
|
cancelPurchaseOrder,
|
||||||
cancelSalesOrder,
|
cancelSalesOrder,
|
||||||
completePurchaseOrder,
|
completePurchaseOrder,
|
||||||
|
completeSalesOrder,
|
||||||
completeShipment,
|
completeShipment,
|
||||||
completePendingShipments,
|
completePendingShipments,
|
||||||
createPurchaseOrder,
|
createPurchaseOrder,
|
||||||
@ -282,6 +283,17 @@ function completePurchaseOrder(order_id, options={}) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: '{% trans "Complete Purchase Order" %}',
|
title: '{% trans "Complete Purchase Order" %}',
|
||||||
confirm: true,
|
confirm: true,
|
||||||
|
fieldsFunction: function(opts) {
|
||||||
|
var fields = {
|
||||||
|
accept_incomplete: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.context.is_complete) {
|
||||||
|
delete fields['accept_incomplete'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
},
|
||||||
preFormContent: function(opts) {
|
preFormContent: function(opts) {
|
||||||
|
|
||||||
var html = `
|
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"
|
* Launches a modal form to mark a SalesOrder as "cancelled"
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user