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
|
||||||
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
|
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
|
v71 -> 2022-08-18 : https://github.com/inventree/InvenTree/pull/3564
|
||||||
- Updates to the "part scheduling" API endpoint
|
- Updates to the "part scheduling" API endpoint
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
"""JSON API for the Order app."""
|
"""JSON API for the Order app."""
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.urls import include, path, re_path
|
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 django_filters import rest_framework as rest_filters
|
||||||
from rest_framework import filters, status
|
from rest_framework import filters, status
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
import order.models as models
|
import order.models as models
|
||||||
@ -116,12 +119,48 @@ class PurchaseOrderList(APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""Save user information on create."""
|
"""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)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
item = serializer.save()
|
with transaction.atomic():
|
||||||
item.created_by = request.user
|
order = serializer.save()
|
||||||
item.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)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
@ -46,6 +46,9 @@
|
|||||||
{% if order.can_cancel %}
|
{% 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>
|
<li><a class='dropdown-item' href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
|
||||||
{% endif %}
|
{% 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||||
@ -217,6 +220,8 @@ $('#print-order-report').click(function() {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if roles.purchase_order.change %}
|
||||||
|
|
||||||
$("#edit-order").click(function() {
|
$("#edit-order").click(function() {
|
||||||
|
|
||||||
editPurchaseOrder({{ order.pk }}, {
|
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() {
|
$("#export-order").click(function() {
|
||||||
exportOrder('{% url "po-export" order.id %}');
|
exportOrder('{% url "po-export" order.id %}');
|
||||||
});
|
});
|
||||||
|
@ -225,6 +225,64 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
expected_code=201
|
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):
|
def test_po_cancel(self):
|
||||||
"""Test the PurchaseOrderCancel API endpoint."""
|
"""Test the PurchaseOrderCancel API endpoint."""
|
||||||
po = models.PurchaseOrder.objects.get(pk=1)
|
po = models.PurchaseOrder.objects.get(pk=1)
|
||||||
|
@ -1049,7 +1049,7 @@ function loadBomTable(table, options={}) {
|
|||||||
available += row.on_order;
|
available += row.on_order;
|
||||||
can_build = available / row.quantity;
|
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;
|
return text;
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
createPurchaseOrderLineItem,
|
createPurchaseOrderLineItem,
|
||||||
createSalesOrder,
|
createSalesOrder,
|
||||||
createSalesOrderShipment,
|
createSalesOrderShipment,
|
||||||
|
duplicatePurchaseOrder,
|
||||||
editPurchaseOrder,
|
editPurchaseOrder,
|
||||||
editPurchaseOrderLineItem,
|
editPurchaseOrderLineItem,
|
||||||
exportOrder,
|
exportOrder,
|
||||||
@ -538,6 +539,39 @@ function purchaseOrderFields(options={}) {
|
|||||||
fields.supplier.hidden = true;
|
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;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,9 +598,20 @@ function createPurchaseOrder(options={}) {
|
|||||||
|
|
||||||
var fields = purchaseOrderFields(options);
|
var fields = purchaseOrderFields(options);
|
||||||
|
|
||||||
|
var groups = {};
|
||||||
|
|
||||||
|
if (options.duplicate_order) {
|
||||||
|
groups.duplicate = {
|
||||||
|
title: '{% trans "Duplication Options" %}',
|
||||||
|
collapsible: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
constructForm('{% url "api-po-list" %}', {
|
constructForm('{% url "api-po-list" %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
fields: fields,
|
fields: fields,
|
||||||
|
groups: groups,
|
||||||
|
data: options.data,
|
||||||
onSuccess: function(data) {
|
onSuccess: function(data) {
|
||||||
|
|
||||||
if (options.onSuccess) {
|
if (options.onSuccess) {
|
||||||
@ -576,7 +621,29 @@ function createPurchaseOrder(options={}) {
|
|||||||
location.href = `/order/purchase-order/${data.pk}/`;
|
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