Add duplicate field to more model types

- Company
- ManufacturerPart
- SupplierPart
- SalesOrderShipment
This commit is contained in:
Oliver Walters 2026-07-02 12:27:22 +00:00
parent 5061e6b935
commit 2c94854253
7 changed files with 105 additions and 7 deletions

View File

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

View File

@ -68,9 +68,6 @@ class BuildSerializer(
):
"""Serializes a Build object."""
export_exclude_fields = ['duplicate']
import_exclude_fields = ['duplicate']
SKIP_CREATE_FIELDS = ['duplicate']
class Meta:

View File

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

View File

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

View File

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

View File

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

View File

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