diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 7ef36f5609..5bbf4fd01e 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -32,6 +32,7 @@ from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, s from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.serializers import ( CustomStatusSerializerMixin, + DuplicateOptionsSerializer, FilterableSerializerMixin, InvenTreeCurrencySerializer, InvenTreeDecimalField, @@ -67,40 +68,6 @@ class TotalPriceMixin(serializers.Serializer): ) -class DuplicateOrderSerializer(serializers.Serializer): - """Serializer for specifying options when duplicating an order.""" - - class Meta: - """Metaclass options.""" - - 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') - ) - - copy_lines = serializers.BooleanField( - required=False, - default=True, - label=_('Copy Lines'), - help_text=_('Copy line items from the original order'), - ) - - copy_extra_lines = serializers.BooleanField( - required=False, - default=True, - label=_('Copy Extra Lines'), - 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( CustomStatusSerializerMixin, DataImportExportSerializerMixin, @@ -195,12 +162,8 @@ class AbstractOrderSerializer( created_by = UserSerializer(read_only=True) - duplicate = DuplicateOrderSerializer( - label=_('Duplicate Order'), - help_text=_('Specify options for duplicating this order'), - required=False, - write_only=True, - ) + # Note: The 'duplicate' field must be defined by each concrete serializer class, + # as it requires a queryset specific to the order model type def validate_reference(self, reference): """Custom validation for the reference field.""" @@ -293,30 +256,21 @@ class AbstractOrderSerializer( instance = super().create(validated_data) if duplicate: - 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) + original = duplicate['original'] - try: - copy_from = instance.__class__.objects.get(pk=order_id) - except Exception: - # If the order ID is invalid, raise a validation error - raise ValidationError(_('Invalid order ID')) - - if copy_lines: - for line in copy_from.lines.all(): + if duplicate.get('copy_lines', False): + for line in original.lines.all(): instance.clean_line_item(line) line.save() - if copy_extra_lines: - for line in copy_from.extra_lines.all(): + if duplicate.get('copy_extra_lines', False): + for line in original.extra_lines.all(): line.pk = None line.order = instance line.save() - if copy_parameters: - instance.copy_parameters_from(copy_from) + if duplicate.get('copy_parameters', False): + instance.copy_parameters_from(original) return instance @@ -452,6 +406,13 @@ class PurchaseOrderSerializer( return [*fields, 'duplicate'] + duplicate = DuplicateOptionsSerializer( + order.models.PurchaseOrder.objects.all(), + copy_lines=True, + copy_extra_lines=True, + copy_parameters=True, + ) + @staticmethod def annotate_queryset(queryset): """Add extra information to the queryset. @@ -1134,6 +1095,13 @@ class SalesOrderSerializer( return [*fields, 'duplicate'] + duplicate = DuplicateOptionsSerializer( + order.models.SalesOrder.objects.all(), + copy_lines=True, + copy_extra_lines=True, + copy_parameters=True, + ) + @staticmethod def annotate_queryset(queryset): """Add extra information to the queryset. @@ -2198,6 +2166,14 @@ class ReturnOrderSerializer( return [*fields, 'duplicate'] + # Note: line items cannot be duplicated from a ReturnOrder, + # as they are linked to specific stock items + duplicate = DuplicateOptionsSerializer( + order.models.ReturnOrder.objects.all(), + copy_extra_lines=True, + copy_parameters=True, + ) + @staticmethod def annotate_queryset(queryset): """Custom annotation for the serializer queryset.""" @@ -2485,6 +2461,11 @@ class TransferOrderSerializer( return [*fields, 'duplicate'] + # Note: TransferOrder does not have "extra" line items + duplicate = DuplicateOptionsSerializer( + order.models.TransferOrder.objects.all(), copy_lines=True, copy_parameters=True + ) + @staticmethod def annotate_queryset(queryset): """Add extra information to the queryset. diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 10b944f40f..e72bd91de5 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -573,7 +573,7 @@ class PurchaseOrderTest(OrderTest): # Duplicate with non-existent PK to provoke error data['duplicate'] = { - 'order_id': 10000001, + 'original': 10000001, 'copy_lines': True, 'copy_extra_lines': False, } @@ -584,7 +584,7 @@ class PurchaseOrderTest(OrderTest): response = self.post(reverse('api-po-list'), data, expected_code=400) data['duplicate'] = { - 'order_id': 1, + 'original': 1, 'copy_lines': True, 'copy_extra_lines': False, } @@ -605,7 +605,7 @@ class PurchaseOrderTest(OrderTest): data['reference'] = 'PO-9998' data['duplicate'] = { - 'order_id': 1, + 'original': 1, 'copy_lines': False, 'copy_extra_lines': True, } @@ -1792,7 +1792,7 @@ class SalesOrderTest(OrderTest): { 'reference': 'SO-12345', 'customer': so.customer.pk, - 'duplicate': {'order_id': so.pk, 'copy_parameters': False}, + 'duplicate': {'original': so.pk, 'copy_parameters': False}, }, ) @@ -1809,7 +1809,7 @@ class SalesOrderTest(OrderTest): { 'reference': 'SO-12346', 'customer': so.customer.pk, - 'duplicate': {'order_id': so.pk}, + 'duplicate': {'original': so.pk}, }, ) diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index a9a87c7e80..c498a719d3 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -407,67 +407,6 @@ class PartBriefSerializer( ) -class DuplicatePartSerializer(serializers.Serializer): - """Serializer for specifying options when duplicating a Part. - - The fields in this serializer control how the Part is duplicated. - """ - - class Meta: - """Metaclass options.""" - - fields = [ - 'part', - 'copy_image', - 'copy_bom', - 'copy_parameters', - 'copy_notes', - 'copy_tests', - ] - - part = serializers.PrimaryKeyRelatedField( - queryset=Part.objects.all(), - label=_('Original Part'), - help_text=_('Select original part to duplicate'), - required=True, - ) - - copy_image = serializers.BooleanField( - label=_('Copy Image'), - help_text=_('Copy image from original part'), - required=False, - default=False, - ) - - copy_bom = serializers.BooleanField( - label=_('Copy BOM'), - help_text=_('Copy bill of materials from original part'), - required=False, - default=False, - ) - - copy_parameters = serializers.BooleanField( - label=_('Copy Parameters'), - help_text=_('Copy parameter data from original part'), - required=False, - default=False, - ) - - copy_notes = serializers.BooleanField( - label=_('Copy Notes'), - help_text=_('Copy notes from original part'), - required=False, - default=True, - ) - - copy_tests = serializers.BooleanField( - label=_('Copy Tests'), - help_text=_('Copy test templates from original part'), - required=False, - default=False, - ) - - class InitialStockSerializer(serializers.Serializer): """Serializer for creating initial stock quantity.""" @@ -1007,11 +946,42 @@ class PartSerializer( ) # Extra fields used only for creation of a new Part instance - duplicate = DuplicatePartSerializer( + duplicate = InvenTree.serializers.DuplicateOptionsSerializer( + Part.objects.all(), label=_('Duplicate Part'), help_text=_('Copy initial data from another Part'), - write_only=True, - required=False, + copy_fields=[ + { + 'name': 'copy_image', + 'label': _('Copy Image'), + 'help_text': _('Copy image from original part'), + 'default': False, + }, + { + 'name': 'copy_bom', + 'label': _('Copy BOM'), + 'help_text': _('Copy bill of materials from original part'), + 'default': False, + }, + { + 'name': 'copy_parameters', + 'label': _('Copy Parameters'), + 'help_text': _('Copy parameter data from original part'), + 'default': False, + }, + { + 'name': 'copy_notes', + 'label': _('Copy Notes'), + 'help_text': _('Copy notes from original part'), + 'default': True, + }, + { + 'name': 'copy_tests', + 'label': _('Copy Tests'), + 'help_text': _('Copy test templates from original part'), + 'default': False, + }, + ], ) initial_stock = InitialStockSerializer( @@ -1077,7 +1047,7 @@ class PartSerializer( # Copy data from original Part if duplicate: - original = duplicate['part'] + original = duplicate['original'] if duplicate.get('copy_bom', False): instance.copy_bom_from(original) diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 011c700884..7752e95dd4 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -1649,7 +1649,7 @@ class PartCreationTests(PartAPITestBase): 'testable': do_copy, 'assembly': do_copy, 'duplicate': { - 'part': 100, + 'original': 100, 'copy_bom': do_copy, 'copy_notes': do_copy, 'copy_image': do_copy, diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx index 311b64b18f..0ead66d6d9 100644 --- a/src/frontend/src/forms/PartForms.tsx +++ b/src/frontend/src/forms/PartForms.tsx @@ -156,7 +156,7 @@ export function usePartFields({ fields.duplicate = { icon: , children: { - part: { + original: { value: duplicatePartInstance?.pk, hidden: true }, diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index aaf4fa83d4..4776e668eb 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -316,7 +316,7 @@ export function usePurchaseOrderFields({ if (!!duplicateOrderId) { fields.duplicate = { children: { - order_id: { + original: { hidden: true, value: duplicateOrderId }, diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index 0fe1866b7b..463e7cc60a 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -84,15 +84,10 @@ export function useReturnOrderFields({ if (!!duplicateOrderId) { fields.duplicate = { children: { - order_id: { + original: { hidden: true, value: duplicateOrderId }, - copy_lines: { - // Cannot duplicate lines from a return order! - value: false, - hidden: true - }, copy_extra_lines: {}, copy_parameters: {} } diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 163861db24..90ec80c034 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -97,7 +97,7 @@ export function useSalesOrderFields({ if (!!duplicateOrderId) { fields.duplicate = { children: { - order_id: { + original: { hidden: true, value: duplicateOrderId }, diff --git a/src/frontend/src/forms/TransferOrderForms.tsx b/src/frontend/src/forms/TransferOrderForms.tsx index a80915e393..a3945fbec5 100644 --- a/src/frontend/src/forms/TransferOrderForms.tsx +++ b/src/frontend/src/forms/TransferOrderForms.tsx @@ -54,13 +54,12 @@ export function useTransferOrderFields({ if (!!duplicateOrderId) { fields.duplicate = { children: { - order_id: { + original: { hidden: true, value: duplicateOrderId }, copy_lines: {}, - // Transfer Orders don't have extra lines for now... - copy_extra_lines: { hidden: true, value: false } + copy_parameters: {} } }; }