From 1ec9795edef9eac56ad112965b3969fde25fe091 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 19 Aug 2022 11:34:37 +1000 Subject: [PATCH] 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 --- InvenTree/InvenTree/api_version.py | 5 +- InvenTree/order/api.py | 47 +++++++++++-- .../order/templates/order/order_base.html | 15 ++++ InvenTree/order/test_api.py | 58 ++++++++++++++++ InvenTree/templates/js/translated/bom.js | 2 +- InvenTree/templates/js/translated/order.js | 69 ++++++++++++++++++- 6 files changed, 189 insertions(+), 7 deletions(-) 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); + } }); }