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.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.

View File

@ -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},
},
)

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):
"""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)

View File

@ -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,

View File

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

View File

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

View File

@ -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: {}
}

View File

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

View File

@ -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: {}
}
};
}