From 27809d712a3b4767213932173a3975e97fa0db54 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 11 Mar 2026 19:26:56 +1100 Subject: [PATCH] Copy order params (#11479) * Copy parameters when duplicating an order * Add unit test for order parameter duplication * Bunmp API version * Fix test reliability * Disable image fetching for SampleSupplierPlugin - Allow turning on manually - Prevent CI issues due to rate limiting * Revery pypdf.. ??? --- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/order/serializers.py | 13 +++- src/backend/InvenTree/order/test_api.py | 59 +++++++++++++++++++ .../samples/supplier/supplier_sample.py | 20 ++++++- src/backend/requirements-3.14.txt | 6 +- src/frontend/src/forms/PurchaseOrderForms.tsx | 3 +- src/frontend/src/forms/ReturnOrderForms.tsx | 3 +- src/frontend/src/forms/SalesOrderForms.tsx | 3 +- 8 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index a41015e398..68b040ae75 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 460 +INVENTREE_API_VERSION = 461 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v461 -> 2026-03-10 : https://github.com/inventree/InvenTree/pull/11479 + - Adds option to copy parameters when duplicating an order via the API + v460 -> 2026-02-25 : https://github.com/inventree/InvenTree/pull/11374 - Adds "updated_at" field to PurchaseOrder, SalesOrder and ReturnOrder API endpoints - Adds "updated_before" and "updated_after" date filters to all three order list endpoints diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 7613323271..7ba6fb674f 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -75,7 +75,7 @@ class DuplicateOrderSerializer(serializers.Serializer): class Meta: """Metaclass options.""" - fields = ['order_id', 'copy_lines', 'copy_extra_lines'] + fields = ['order_id', 'copy_lines', 'copy_extra_lines', 'copy_parameters'] order_id = serializers.IntegerField( required=True, label=_('Order ID'), help_text=_('ID of the order to duplicate') @@ -95,6 +95,13 @@ class DuplicateOrderSerializer(serializers.Serializer): help_text=_('Copy extra line items from the original order'), ) + copy_parameters = serializers.BooleanField( + required=False, + default=True, + label=_('Copy Parameters'), + help_text=_('Copy order parameters from the original order'), + ) + class AbstractOrderSerializer( DataImportExportSerializerMixin, FilterableSerializerMixin, serializers.Serializer @@ -242,6 +249,7 @@ class AbstractOrderSerializer( order_id = duplicate.get('order_id', None) copy_lines = duplicate.get('copy_lines', True) copy_extra_lines = duplicate.get('copy_extra_lines', True) + copy_parameters = duplicate.get('copy_parameters', True) try: copy_from = instance.__class__.objects.get(pk=order_id) @@ -260,6 +268,9 @@ class AbstractOrderSerializer( line.order = instance line.save() + if copy_parameters: + instance.copy_parameters_from(copy_from) + return instance diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 9b3a4283de..7952f29eac 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -1574,6 +1574,65 @@ class SalesOrderTest(OrderTest): expected_code=201, ) + def test_so_duplicate(self): + """Test SalesOrder duplication via the API.""" + from common.models import Parameter, ParameterTemplate + + url = reverse('api-so-list') + + self.assignRole('sales_order.add') + + so = models.SalesOrder.objects.get(pk=1) + self.assertEqual(so.status, SalesOrderStatus.PENDING) + + # Add some parameters to the sales order + for idx in range(5): + template = ParameterTemplate.objects.create(name=f'Template {idx}') + + Parameter.objects.create( + template=template, + model_type=so.get_content_type(), + model_id=so.pk, + data=f'Value {idx}', + ) + + self.assertEqual(so.parameters.count(), 5) + + # Create a duplicate of this sales order + # We explicitly specify "copy_parameters" as False, so the duplicated sales order should not have any parameters + response = self.post( + url, + { + 'reference': 'SO-12345', + 'customer': so.customer.pk, + 'duplicate': {'order_id': so.pk, 'copy_parameters': False}, + }, + ) + + duplicate_id = response.data['pk'] + duplicate_so = models.SalesOrder.objects.get(pk=duplicate_id) + + self.assertEqual(duplicate_so.reference, 'SO-12345') + self.assertEqual(duplicate_so.customer, so.customer) + self.assertEqual(duplicate_so.parameters.count(), 0) + + # Duplicate again, with default values for the "duplicate" options (which should result in parameters being copied) + response = self.post( + url, + { + 'reference': 'SO-12346', + 'customer': so.customer.pk, + 'duplicate': {'order_id': so.pk}, + }, + ) + + duplicate_id = response.data['pk'] + duplicate_so = models.SalesOrder.objects.get(pk=duplicate_id) + + self.assertEqual(duplicate_so.reference, 'SO-12346') + self.assertEqual(duplicate_so.customer, so.customer) + self.assertEqual(duplicate_so.parameters.count(), 5) + def test_so_cancel(self): """Test API endpoint for cancelling a SalesOrder.""" so = models.SalesOrder.objects.get(pk=1) diff --git a/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py b/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py index 9fe6d8ea00..c27476f83e 100644 --- a/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py +++ b/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py @@ -1,5 +1,7 @@ """Sample supplier plugin.""" +from django.conf import settings + from company.models import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak from part.models import Part from plugin.mixins import SupplierMixin, supplier @@ -13,7 +15,16 @@ class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin): SLUG = 'samplesupplier' TITLE = 'My sample supplier plugin' - VERSION = '0.0.1' + VERSION = '0.0.2' + + SETTINGS = { + 'DOWNLOAD_IMAGES': { + 'name': 'Download part images', + 'description': 'Enable downloading of part images during import (not recommended during testing)', + 'validator': bool, + 'default': False, + } + } def __init__(self): """Initialize the sample supplier plugin.""" @@ -108,7 +119,12 @@ class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin): # If the part was created, set additional fields if created: - if data['image_url']: + # Prevent downloading images during testing, as this can lead to unreliable tests + if ( + data['image_url'] + and not settings.TESTING + and self.get_setting('DOWNLOAD_IMAGES') + ): file, fmt = self.download_image(data['image_url']) filename = f'part_{part.pk}_image.{fmt.lower()}' part.image.save(filename, file) diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index b835bd5077..d56202ad6d 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -1669,9 +1669,9 @@ pynacl==1.6.2 \ # via # -c src/backend/requirements.txt # paramiko -pypdf==6.8.0 \ - --hash=sha256:2a025080a8dd73f48123c89c57174a5ff3806c71763ee4e49572dc90454943c7 \ - --hash=sha256:cb7eaeaa4133ce76f762184069a854e03f4d9a08568f0e0623f7ea810407833b +pypdf==6.7.5 \ + --hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \ + --hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d # via # -c src/backend/requirements.txt # -r src/backend/requirements.in diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index b83f1e8d8b..27dc17d086 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -292,7 +292,8 @@ export function usePurchaseOrderFields({ value: duplicateOrderId }, copy_lines: {}, - copy_extra_lines: {} + copy_extra_lines: {}, + copy_parameters: {} } }; } diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index 92f631feab..ca4e5a51f6 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -91,7 +91,8 @@ export function useReturnOrderFields({ value: false, hidden: true }, - copy_extra_lines: {} + copy_extra_lines: {}, + copy_parameters: {} } }; } diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 5adab0a6f6..1a060df877 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -90,7 +90,8 @@ export function useSalesOrderFields({ value: duplicateOrderId }, copy_lines: {}, - copy_extra_lines: {} + copy_extra_lines: {}, + copy_parameters: {} } }; }