Refactor existing implementations

This commit is contained in:
Oliver Walters 2026-07-02 11:13:35 +00:00
parent 0d16a17fe3
commit 0943733328
9 changed files with 84 additions and 139 deletions

View File

@ -32,6 +32,7 @@ from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, s
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import ( from InvenTree.serializers import (
CustomStatusSerializerMixin, CustomStatusSerializerMixin,
DuplicateOptionsSerializer,
FilterableSerializerMixin, FilterableSerializerMixin,
InvenTreeCurrencySerializer, InvenTreeCurrencySerializer,
InvenTreeDecimalField, 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( class AbstractOrderSerializer(
CustomStatusSerializerMixin, CustomStatusSerializerMixin,
DataImportExportSerializerMixin, DataImportExportSerializerMixin,
@ -195,12 +162,8 @@ class AbstractOrderSerializer(
created_by = UserSerializer(read_only=True) created_by = UserSerializer(read_only=True)
duplicate = DuplicateOrderSerializer( # Note: The 'duplicate' field must be defined by each concrete serializer class,
label=_('Duplicate Order'), # as it requires a queryset specific to the order model type
help_text=_('Specify options for duplicating this order'),
required=False,
write_only=True,
)
def validate_reference(self, reference): def validate_reference(self, reference):
"""Custom validation for the reference field.""" """Custom validation for the reference field."""
@ -293,30 +256,21 @@ class AbstractOrderSerializer(
instance = super().create(validated_data) instance = super().create(validated_data)
if duplicate: if duplicate:
order_id = duplicate.get('order_id', None) original = duplicate['original']
copy_lines = duplicate.get('copy_lines', True)
copy_extra_lines = duplicate.get('copy_extra_lines', True)
copy_parameters = duplicate.get('copy_parameters', True)
try: if duplicate.get('copy_lines', False):
copy_from = instance.__class__.objects.get(pk=order_id) for line in original.lines.all():
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():
instance.clean_line_item(line) instance.clean_line_item(line)
line.save() line.save()
if copy_extra_lines: if duplicate.get('copy_extra_lines', False):
for line in copy_from.extra_lines.all(): for line in original.extra_lines.all():
line.pk = None line.pk = None
line.order = instance line.order = instance
line.save() line.save()
if copy_parameters: if duplicate.get('copy_parameters', False):
instance.copy_parameters_from(copy_from) instance.copy_parameters_from(original)
return instance return instance
@ -452,6 +406,13 @@ class PurchaseOrderSerializer(
return [*fields, 'duplicate'] return [*fields, 'duplicate']
duplicate = DuplicateOptionsSerializer(
order.models.PurchaseOrder.objects.all(),
copy_lines=True,
copy_extra_lines=True,
copy_parameters=True,
)
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Add extra information to the queryset. """Add extra information to the queryset.
@ -1134,6 +1095,13 @@ class SalesOrderSerializer(
return [*fields, 'duplicate'] return [*fields, 'duplicate']
duplicate = DuplicateOptionsSerializer(
order.models.SalesOrder.objects.all(),
copy_lines=True,
copy_extra_lines=True,
copy_parameters=True,
)
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Add extra information to the queryset. """Add extra information to the queryset.
@ -2198,6 +2166,14 @@ class ReturnOrderSerializer(
return [*fields, 'duplicate'] 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 @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Custom annotation for the serializer queryset.""" """Custom annotation for the serializer queryset."""
@ -2485,6 +2461,11 @@ class TransferOrderSerializer(
return [*fields, 'duplicate'] 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 @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Add extra information to the queryset. """Add extra information to the queryset.

View File

@ -573,7 +573,7 @@ class PurchaseOrderTest(OrderTest):
# Duplicate with non-existent PK to provoke error # Duplicate with non-existent PK to provoke error
data['duplicate'] = { data['duplicate'] = {
'order_id': 10000001, 'original': 10000001,
'copy_lines': True, 'copy_lines': True,
'copy_extra_lines': False, 'copy_extra_lines': False,
} }
@ -584,7 +584,7 @@ class PurchaseOrderTest(OrderTest):
response = self.post(reverse('api-po-list'), data, expected_code=400) response = self.post(reverse('api-po-list'), data, expected_code=400)
data['duplicate'] = { data['duplicate'] = {
'order_id': 1, 'original': 1,
'copy_lines': True, 'copy_lines': True,
'copy_extra_lines': False, 'copy_extra_lines': False,
} }
@ -605,7 +605,7 @@ class PurchaseOrderTest(OrderTest):
data['reference'] = 'PO-9998' data['reference'] = 'PO-9998'
data['duplicate'] = { data['duplicate'] = {
'order_id': 1, 'original': 1,
'copy_lines': False, 'copy_lines': False,
'copy_extra_lines': True, 'copy_extra_lines': True,
} }
@ -1792,7 +1792,7 @@ class SalesOrderTest(OrderTest):
{ {
'reference': 'SO-12345', 'reference': 'SO-12345',
'customer': so.customer.pk, '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', 'reference': 'SO-12346',
'customer': so.customer.pk, 'customer': so.customer.pk,
'duplicate': {'order_id': so.pk}, 'duplicate': {'original': so.pk},
}, },
) )

View File

@ -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): class InitialStockSerializer(serializers.Serializer):
"""Serializer for creating initial stock quantity.""" """Serializer for creating initial stock quantity."""
@ -1007,11 +946,42 @@ class PartSerializer(
) )
# Extra fields used only for creation of a new Part instance # Extra fields used only for creation of a new Part instance
duplicate = DuplicatePartSerializer( duplicate = InvenTree.serializers.DuplicateOptionsSerializer(
Part.objects.all(),
label=_('Duplicate Part'), label=_('Duplicate Part'),
help_text=_('Copy initial data from another Part'), help_text=_('Copy initial data from another Part'),
write_only=True, copy_fields=[
required=False, {
'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( initial_stock = InitialStockSerializer(
@ -1077,7 +1047,7 @@ class PartSerializer(
# Copy data from original Part # Copy data from original Part
if duplicate: if duplicate:
original = duplicate['part'] original = duplicate['original']
if duplicate.get('copy_bom', False): if duplicate.get('copy_bom', False):
instance.copy_bom_from(original) instance.copy_bom_from(original)

View File

@ -1649,7 +1649,7 @@ class PartCreationTests(PartAPITestBase):
'testable': do_copy, 'testable': do_copy,
'assembly': do_copy, 'assembly': do_copy,
'duplicate': { 'duplicate': {
'part': 100, 'original': 100,
'copy_bom': do_copy, 'copy_bom': do_copy,
'copy_notes': do_copy, 'copy_notes': do_copy,
'copy_image': do_copy, 'copy_image': do_copy,

View File

@ -156,7 +156,7 @@ export function usePartFields({
fields.duplicate = { fields.duplicate = {
icon: <IconCopy />, icon: <IconCopy />,
children: { children: {
part: { original: {
value: duplicatePartInstance?.pk, value: duplicatePartInstance?.pk,
hidden: true hidden: true
}, },

View File

@ -316,7 +316,7 @@ export function usePurchaseOrderFields({
if (!!duplicateOrderId) { if (!!duplicateOrderId) {
fields.duplicate = { fields.duplicate = {
children: { children: {
order_id: { original: {
hidden: true, hidden: true,
value: duplicateOrderId value: duplicateOrderId
}, },

View File

@ -84,15 +84,10 @@ export function useReturnOrderFields({
if (!!duplicateOrderId) { if (!!duplicateOrderId) {
fields.duplicate = { fields.duplicate = {
children: { children: {
order_id: { original: {
hidden: true, hidden: true,
value: duplicateOrderId value: duplicateOrderId
}, },
copy_lines: {
// Cannot duplicate lines from a return order!
value: false,
hidden: true
},
copy_extra_lines: {}, copy_extra_lines: {},
copy_parameters: {} copy_parameters: {}
} }

View File

@ -97,7 +97,7 @@ export function useSalesOrderFields({
if (!!duplicateOrderId) { if (!!duplicateOrderId) {
fields.duplicate = { fields.duplicate = {
children: { children: {
order_id: { original: {
hidden: true, hidden: true,
value: duplicateOrderId value: duplicateOrderId
}, },

View File

@ -54,13 +54,12 @@ export function useTransferOrderFields({
if (!!duplicateOrderId) { if (!!duplicateOrderId) {
fields.duplicate = { fields.duplicate = {
children: { children: {
order_id: { original: {
hidden: true, hidden: true,
value: duplicateOrderId value: duplicateOrderId
}, },
copy_lines: {}, copy_lines: {},
// Transfer Orders don't have extra lines for now... copy_parameters: {}
copy_extra_lines: { hidden: true, value: false }
} }
}; };
} }