diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index aad384331d..9a3b1cef2f 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -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
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index 72a47e113a..0a625a89c3 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -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)
diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html
index 000d4c7751..cbb82640db 100644
--- a/InvenTree/order/templates/order/order_base.html
+++ b/InvenTree/order/templates/order/order_base.html
@@ -46,6 +46,9 @@
{% if order.can_cancel %}
{% trans "Cancel order" %}
{% endif %}
+ {% if roles.purchase_order.add %}
+ {% trans "Duplicate order" %}
+ {% endif %}
{% 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 %}');
});
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index 7824fcb4a7..3e5f63c72a 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -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)
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 6193b5a127..ccee63e30f 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -1049,7 +1049,7 @@ function loadBomTable(table, options={}) {
available += row.on_order;
can_build = available / row.quantity;
- text += ``;
+ text += ``;
}
return text;
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index fd9421a6e8..61482acf09 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -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);
+ }
});
}