mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Adds option for duplicating an existing purchase order (#3567)
* Adds option for duplicating an existing purchase order - Copy line items across * Update API version * JS linting * Adds option for duplication of extra lines * Decimal point rounding fix * Unit tests for PO duplication via the API
This commit is contained in:
parent
868697392f
commit
1ec9795ede
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 71
|
||||
INVENTREE_API_VERSION = 72
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v72 -> 2022-08-18 : https://github.com/inventree/InvenTree/pull/3567
|
||||
- Allow PurchaseOrder to be duplicated via the API
|
||||
|
||||
v71 -> 2022-08-18 : https://github.com/inventree/InvenTree/pull/3564
|
||||
- Updates to the "part scheduling" API endpoint
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
"""JSON API for the Order app."""
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
from django.urls import include, path, re_path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from rest_framework import filters, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
import order.models as models
|
||||
@ -116,12 +119,48 @@ class PurchaseOrderList(APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Save user information on create."""
|
||||
serializer = self.get_serializer(data=self.clean_data(request.data))
|
||||
|
||||
data = self.clean_data(request.data)
|
||||
|
||||
duplicate_order = data.pop('duplicate_order', None)
|
||||
duplicate_line_items = str2bool(data.pop('duplicate_line_items', False))
|
||||
duplicate_extra_lines = str2bool(data.pop('duplicate_extra_lines', False))
|
||||
|
||||
if duplicate_order is not None:
|
||||
try:
|
||||
duplicate_order = models.PurchaseOrder.objects.get(pk=duplicate_order)
|
||||
except (ValueError, models.PurchaseOrder.DoesNotExist):
|
||||
raise ValidationError({
|
||||
'duplicate_order': [_('No matching purchase order found')],
|
||||
})
|
||||
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
item = serializer.save()
|
||||
item.created_by = request.user
|
||||
item.save()
|
||||
with transaction.atomic():
|
||||
order = serializer.save()
|
||||
order.created_by = request.user
|
||||
order.save()
|
||||
|
||||
# Duplicate line items from other order if required
|
||||
if duplicate_order is not None:
|
||||
|
||||
if duplicate_line_items:
|
||||
for line in duplicate_order.lines.all():
|
||||
# Copy the line across to the new order
|
||||
line.pk = None
|
||||
line.order = order
|
||||
line.received = 0
|
||||
|
||||
line.save()
|
||||
|
||||
if duplicate_extra_lines:
|
||||
for line in duplicate_order.extra_lines.all():
|
||||
# Copy the line across to the new order
|
||||
line.pk = None
|
||||
line.order = order
|
||||
|
||||
line.save()
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
@ -46,6 +46,9 @@
|
||||
{% if order.can_cancel %}
|
||||
<li><a class='dropdown-item' href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a class='dropdown-item' href='#' id='duplicate-order'><span class='fas fa-clone'></span> {% trans "Duplicate order" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||
@ -217,6 +220,8 @@ $('#print-order-report').click(function() {
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if roles.purchase_order.change %}
|
||||
|
||||
$("#edit-order").click(function() {
|
||||
|
||||
editPurchaseOrder({{ order.pk }}, {
|
||||
@ -275,6 +280,16 @@ $("#cancel-order").click(function() {
|
||||
);
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if roles.purchase_order.add %}
|
||||
$('#duplicate-order').click(function() {
|
||||
duplicatePurchaseOrder(
|
||||
{{ order.pk }},
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$("#export-order").click(function() {
|
||||
exportOrder('{% url "po-export" order.id %}');
|
||||
});
|
||||
|
@ -225,6 +225,64 @@ class PurchaseOrderTest(OrderTest):
|
||||
expected_code=201
|
||||
)
|
||||
|
||||
def test_po_duplicate(self):
|
||||
"""Test that we can duplicate a PurchaseOrder via the API"""
|
||||
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
po = models.PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertTrue(po.lines.count() > 0)
|
||||
|
||||
# Add some extra line items to this order
|
||||
for idx in range(5):
|
||||
models.PurchaseOrderExtraLine.objects.create(
|
||||
order=po,
|
||||
quantity=idx + 10,
|
||||
reference='some reference',
|
||||
)
|
||||
|
||||
data = self.get(reverse('api-po-detail', kwargs={'pk': 1})).data
|
||||
|
||||
del data['pk']
|
||||
del data['reference']
|
||||
|
||||
data['duplicate_order'] = 1
|
||||
data['duplicate_line_items'] = True
|
||||
data['duplicate_extra_lines'] = False
|
||||
|
||||
data['reference'] = 'PO-9999'
|
||||
|
||||
# Duplicate via the API
|
||||
response = self.post(
|
||||
reverse('api-po-list'),
|
||||
data,
|
||||
expected_code=201
|
||||
)
|
||||
|
||||
# Order is for the same supplier
|
||||
self.assertEqual(response.data['supplier'], po.supplier.pk)
|
||||
|
||||
po_dup = models.PurchaseOrder.objects.get(pk=response.data['pk'])
|
||||
|
||||
self.assertEqual(po_dup.extra_lines.count(), 0)
|
||||
self.assertEqual(po_dup.lines.count(), po.lines.count())
|
||||
|
||||
data['reference'] = 'PO-9998'
|
||||
data['duplicate_line_items'] = False
|
||||
data['duplicate_extra_lines'] = True
|
||||
|
||||
response = self.post(
|
||||
reverse('api-po-list'),
|
||||
data,
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
po_dup = models.PurchaseOrder.objects.get(pk=response.data['pk'])
|
||||
|
||||
self.assertEqual(po_dup.extra_lines.count(), po.extra_lines.count())
|
||||
self.assertEqual(po_dup.lines.count(), 0)
|
||||
|
||||
def test_po_cancel(self):
|
||||
"""Test the PurchaseOrderCancel API endpoint."""
|
||||
po = models.PurchaseOrder.objects.get(pk=1)
|
||||
|
@ -1049,7 +1049,7 @@ function loadBomTable(table, options={}) {
|
||||
available += row.on_order;
|
||||
can_build = available / row.quantity;
|
||||
|
||||
text += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Including On Order" %}: ${can_build}'></span>`;
|
||||
text += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Including On Order" %}: ${formatDecimal(can_build, 2)}'></span>`;
|
||||
}
|
||||
|
||||
return text;
|
||||
|
@ -30,6 +30,7 @@
|
||||
createPurchaseOrderLineItem,
|
||||
createSalesOrder,
|
||||
createSalesOrderShipment,
|
||||
duplicatePurchaseOrder,
|
||||
editPurchaseOrder,
|
||||
editPurchaseOrderLineItem,
|
||||
exportOrder,
|
||||
@ -538,6 +539,39 @@ function purchaseOrderFields(options={}) {
|
||||
fields.supplier.hidden = true;
|
||||
}
|
||||
|
||||
// Add fields for order duplication (only if required)
|
||||
if (options.duplicate_order) {
|
||||
fields.duplicate_order = {
|
||||
value: options.duplicate_order,
|
||||
group: 'duplicate',
|
||||
required: 'true',
|
||||
type: 'related field',
|
||||
model: 'purchaseorder',
|
||||
filters: {
|
||||
supplier_detail: true,
|
||||
},
|
||||
api_url: '{% url "api-po-list" %}',
|
||||
label: '{% trans "Purchase Order" %}',
|
||||
help_text: '{% trans "Select purchase order to duplicate" %}',
|
||||
};
|
||||
|
||||
fields.duplicate_line_items = {
|
||||
value: true,
|
||||
group: 'duplicate',
|
||||
type: 'boolean',
|
||||
label: '{% trans "Duplicate Line Items" %}',
|
||||
help_text: '{% trans "Duplicate all line items from the selected order" %}',
|
||||
};
|
||||
|
||||
fields.duplicate_extra_lines = {
|
||||
value: true,
|
||||
group: 'duplicate',
|
||||
type: 'boolean',
|
||||
label: '{% trans "Duplicate Extra Lines" %}',
|
||||
help_text: '{% trans "Duplicate extra line items from the selected order" %}',
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -564,9 +598,20 @@ function createPurchaseOrder(options={}) {
|
||||
|
||||
var fields = purchaseOrderFields(options);
|
||||
|
||||
var groups = {};
|
||||
|
||||
if (options.duplicate_order) {
|
||||
groups.duplicate = {
|
||||
title: '{% trans "Duplication Options" %}',
|
||||
collapsible: false,
|
||||
};
|
||||
};
|
||||
|
||||
constructForm('{% url "api-po-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
groups: groups,
|
||||
data: options.data,
|
||||
onSuccess: function(data) {
|
||||
|
||||
if (options.onSuccess) {
|
||||
@ -576,7 +621,29 @@ function createPurchaseOrder(options={}) {
|
||||
location.href = `/order/purchase-order/${data.pk}/`;
|
||||
}
|
||||
},
|
||||
title: '{% trans "Create Purchase Order" %}',
|
||||
title: options.title || '{% trans "Create Purchase Order" %}',
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Duplicate an existing PurchaseOrder
|
||||
* Provides user with option to duplicate line items for the order also.
|
||||
*/
|
||||
function duplicatePurchaseOrder(order_id, options={}) {
|
||||
|
||||
options.duplicate_order = order_id;
|
||||
|
||||
inventreeGet(`/api/order/po/${order_id}/`, {}, {
|
||||
success: function(data) {
|
||||
|
||||
// Clear out data we do not want to be duplicated
|
||||
delete data['pk'];
|
||||
delete data['reference'];
|
||||
|
||||
options.data = data;
|
||||
|
||||
createPurchaseOrder(options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user