From 2c94854253870ad0ededb4fd34c52b06a8fe0660 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 2 Jul 2026 12:27:22 +0000 Subject: [PATCH] Add duplicate field to more model types - Company - ManufacturerPart - SupplierPart - SalesOrderShipment --- .../InvenTree/InvenTree/api_version.py | 2 +- src/backend/InvenTree/build/serializers.py | 3 - src/backend/InvenTree/company/serializers.py | 60 +++++++++++++++++++ src/backend/InvenTree/data_exporter/mixins.py | 9 +++ src/backend/InvenTree/importer/mixins.py | 10 ++++ src/backend/InvenTree/order/serializers.py | 26 +++++++- src/backend/InvenTree/part/serializers.py | 2 +- 7 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index af26ba1caf..26ce0c47d7 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -7,7 +7,7 @@ INVENTREE_API_VERSION = 514 INVENTREE_API_TEXT = """ v514 -> 2026-07-02 : https://github.com/inventree/InvenTree/pull/12294 - - Adds "duplicate" field to the BuildOrder API endpoint, allowing duplication options to be specified when creating a new build + - Adds "duplicate" field to the BuildOrder, Company, ManufacturerPart, SupplierPart and SalesOrderShipment API endpoints - Order duplication options: renames "order_id" field to "original", which now performs primary-key validation - Part duplication options: renames "part" field to "original" diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index ffb846507e..3864061b7d 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -68,9 +68,6 @@ class BuildSerializer( ): """Serializes a Build object.""" - export_exclude_fields = ['duplicate'] - import_exclude_fields = ['duplicate'] - SKIP_CREATE_FIELDS = ['duplicate'] class Meta: diff --git a/src/backend/InvenTree/company/serializers.py b/src/backend/InvenTree/company/serializers.py index 1aa191e966..809f06710f 100644 --- a/src/backend/InvenTree/company/serializers.py +++ b/src/backend/InvenTree/company/serializers.py @@ -1,5 +1,6 @@ """JSON serializers for Company app.""" +from django.db import transaction from django.db.models import Prefetch from django.utils.translation import gettext_lazy as _ @@ -14,6 +15,7 @@ from importer.registry import register_importer from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import ( + DuplicateOptionsSerializer, FilterableSerializerMixin, InvenTreeCurrencySerializer, InvenTreeDecimalField, @@ -118,6 +120,8 @@ class CompanySerializer( import_exclude_fields = ['image'] + SKIP_CREATE_FIELDS = ['duplicate'] + class Meta: """Metaclass options.""" @@ -132,6 +136,7 @@ class CompanySerializer( 'email', 'currency', 'contact', + 'duplicate', 'link', 'image', 'active', @@ -191,6 +196,23 @@ class CompanySerializer( parameters = common.filters.enable_parameters_filter() + duplicate = DuplicateOptionsSerializer(Company.objects.all(), copy_parameters=True) + + @transaction.atomic + def create(self, validated_data): + """Create a new Company instance, optionally copying data from an existing company.""" + duplicate = validated_data.pop('duplicate', None) + + instance = super().create(validated_data) + + if duplicate: + original = duplicate['original'] + + if duplicate.get('copy_parameters', True): + instance.copy_parameters_from(original) + + return instance + @register_importer() class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): @@ -217,6 +239,8 @@ class ManufacturerPartSerializer( ): """Serializer for ManufacturerPart object.""" + SKIP_CREATE_FIELDS = ['duplicate'] + class Meta: """Metaclass options.""" @@ -229,6 +253,7 @@ class ManufacturerPartSerializer( 'manufacturer', 'manufacturer_detail', 'description', + 'duplicate', 'MPN', 'link', 'barcode_hash', @@ -241,6 +266,25 @@ class ManufacturerPartSerializer( parameters = common.filters.enable_parameters_filter() + duplicate = DuplicateOptionsSerializer( + ManufacturerPart.objects.all(), copy_parameters=True + ) + + @transaction.atomic + def create(self, validated_data): + """Create a new ManufacturerPart instance, optionally copying data from an existing instance.""" + duplicate = validated_data.pop('duplicate', None) + + instance = super().create(validated_data) + + if duplicate: + original = duplicate['original'] + + if duplicate.get('copy_parameters', True): + instance.copy_parameters_from(original) + + return instance + part_detail = OptionalField( serializer_class=part_serializers.PartBriefSerializer, serializer_kwargs={ @@ -323,6 +367,8 @@ class SupplierPartSerializer( export_exclude_fields = ['tags'] + SKIP_CREATE_FIELDS = ['duplicate'] + export_child_fields = [ 'part_detail.name', 'part_detail.description', @@ -339,6 +385,7 @@ class SupplierPartSerializer( 'available', 'availability_updated', 'description', + 'duplicate', 'in_stock', 'on_order', 'link', @@ -494,6 +541,10 @@ class SupplierPartSerializer( # Date fields updated = serializers.DateTimeField(allow_null=True, read_only=True) + duplicate = DuplicateOptionsSerializer( + SupplierPart.objects.all(), copy_parameters=True + ) + @staticmethod def annotate_queryset(queryset): """Annotate the SupplierPart queryset with extra fields. @@ -522,8 +573,11 @@ class SupplierPartSerializer( return response + @transaction.atomic def create(self, validated_data): """Extract manufacturer data and process ManufacturerPart.""" + duplicate = validated_data.pop('duplicate', None) + # Extract 'available' quantity from the serializer available = validated_data.pop('available', None) @@ -541,6 +595,12 @@ class SupplierPartSerializer( kwargs = {'manufacturer': manufacturer, 'MPN': MPN} supplier_part.save(**kwargs) + if duplicate: + original = duplicate['original'] + + if duplicate.get('copy_parameters', True): + supplier_part.copy_parameters_from(original) + return supplier_part diff --git a/src/backend/InvenTree/data_exporter/mixins.py b/src/backend/InvenTree/data_exporter/mixins.py index 0d58d0efff..af1b971db8 100644 --- a/src/backend/InvenTree/data_exporter/mixins.py +++ b/src/backend/InvenTree/data_exporter/mixins.py @@ -16,6 +16,7 @@ from taggit.serializers import TagListSerializerField import data_exporter.serializers import data_exporter.tasks import InvenTree.exceptions +import InvenTree.serializers from common.models import DataOutput from InvenTree.helpers import str2bool from InvenTree.tasks import offload_task @@ -64,6 +65,14 @@ class DataExportSerializerMixin: # Exclude fields which are not required for data export for field in self.get_export_exclude_fields(**kwargs): self.fields.pop(field, None) + + # Duplication options are never used for data export + for field in [ + name + for name, field in self.fields.items() + if isinstance(field, InvenTree.serializers.DuplicateOptionsSerializer) + ]: + self.fields.pop(field, None) else: # Exclude fields which are only used for data export for field in self.get_export_only_fields(**kwargs): diff --git a/src/backend/InvenTree/importer/mixins.py b/src/backend/InvenTree/importer/mixins.py index 969f82afa2..9cd5401606 100644 --- a/src/backend/InvenTree/importer/mixins.py +++ b/src/backend/InvenTree/importer/mixins.py @@ -3,6 +3,8 @@ from rest_framework import fields, serializers from taggit.serializers import TagListSerializerField +import InvenTree.serializers + class DataImportSerializerMixin: """Mixin class for adding data import functionality to a DRF serializer.""" @@ -41,6 +43,14 @@ class DataImportSerializerMixin: for field in self.get_import_exclude_fields(**kwargs): self.fields.pop(field, None) + # Duplication options are never used for data import + for field in [ + name + for name, field in self.fields.items() + if isinstance(field, InvenTree.serializers.DuplicateOptionsSerializer) + ]: + self.fields.pop(field, None) + else: # Exclude fields which are only used for data import for field in self.get_import_only_fields(**kwargs): diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 5bbf4fd01e..04dc6e79f4 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -77,9 +77,9 @@ class AbstractOrderSerializer( ): """Abstract serializer class which provides fields common to all order types.""" - export_exclude_fields = ['notes', 'duplicate'] + export_exclude_fields = ['notes'] - import_exclude_fields = ['notes', 'duplicate'] + import_exclude_fields = ['notes'] # Number of line items in this order line_items = serializers.IntegerField( @@ -1379,6 +1379,8 @@ class SalesOrderShipmentSerializer( ): """Serializer for the SalesOrderShipment class.""" + SKIP_CREATE_FIELDS = ['duplicate'] + class Meta: """Metaclass options.""" @@ -1391,6 +1393,7 @@ class SalesOrderShipmentSerializer( 'shipment_address', 'delivery_date', 'checked_by', + 'duplicate', 'reference', 'tracking_number', 'invoice_number', @@ -1475,6 +1478,25 @@ class SalesOrderShipmentSerializer( tags = common.filters.enable_tags_filter() + duplicate = DuplicateOptionsSerializer( + order.models.SalesOrderShipment.objects.all(), copy_parameters=True + ) + + @transaction.atomic + def create(self, validated_data): + """Create a new SalesOrderShipment instance, optionally copying data from an existing shipment.""" + duplicate = validated_data.pop('duplicate', None) + + instance = super().create(validated_data) + + if duplicate: + original = duplicate['original'] + + if duplicate.get('copy_parameters', True): + instance.copy_parameters_from(original) + + return instance + class SalesOrderAllocationSerializer( FilterableSerializerMixin, InvenTreeModelSerializer diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index c498a719d3..bbda3ee510 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -540,7 +540,7 @@ class PartSerializer( Used when displaying all details of a single component. """ - import_exclude_fields = ['creation_date', 'creation_user', 'duplicate'] + import_exclude_fields = ['creation_date', 'creation_user'] class Meta: """Metaclass defining serializer fields."""