mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-04 06:00:38 +00:00
[API] Refactor API for duplicating objects (#12294)
* Implement generic serializer for custom model duplication * Apply pattern to BuildOrder * Add more generic options * Bump API version * Adjust default option * Refactor existing implementations * Dynamic class typing * Add duplicate field to more model types - Company - ManufacturerPart - SupplierPart - SalesOrderShipment * Implement parameter duplication for more models: - Company - ManufacturerPart - SupplierPart * Simplify code * Refactor
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 513
|
INVENTREE_API_VERSION = 514
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v514 -> 2026-07-02 : https://github.com/inventree/InvenTree/pull/12294
|
||||||
|
- 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"
|
||||||
|
|
||||||
v513 -> 2026-06-25 : https://github.com/inventree/InvenTree/pull/12250
|
v513 -> 2026-06-25 : https://github.com/inventree/InvenTree/pull/12250
|
||||||
- Adds "active" field to the ProjectCode model and API endpoints
|
- Adds "active" field to the ProjectCode model and API endpoints
|
||||||
|
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
Default implementation returns an empty list
|
Default implementation returns an empty list
|
||||||
"""
|
"""
|
||||||
return []
|
return getattr(self, 'SKIP_CREATE_FIELDS', [])
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
"""Catch any django ValidationError thrown at the moment `save` is called, and re-throw as a DRF ValidationError."""
|
"""Catch any django ValidationError thrown at the moment `save` is called, and re-throw as a DRF ValidationError."""
|
||||||
@@ -921,3 +921,104 @@ class ContentTypeField(serializers.ChoiceField):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return content_type
|
return content_type
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateOptionsSerializer(serializers.Serializer):
|
||||||
|
"""Generic serializer for specifying copy options when duplicating a model instance.
|
||||||
|
|
||||||
|
Builds its fields dynamically at instantiation time so the same class can be
|
||||||
|
reused for any model without subclassing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Special 'shortcut' fields which are used for multiple models
|
||||||
|
DEFAULT_FIELDS = [
|
||||||
|
(
|
||||||
|
'copy_parameters',
|
||||||
|
_('Copy Parameters'),
|
||||||
|
_('Copy parameters from the original item'),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'copy_lines',
|
||||||
|
_('Copy Lines'),
|
||||||
|
_('Copy line items from the original order'),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'copy_extra_lines',
|
||||||
|
_('Copy Extra Lines'),
|
||||||
|
_('Copy extra line items from the original order'),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
queryset: QuerySet,
|
||||||
|
*args,
|
||||||
|
copy_fields: Optional[list[dict]] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Initialise the serializer and dynamically attach fields.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
queryset: Queryset used for the `original` PrimaryKeyRelatedField.
|
||||||
|
copy_fields: Optional list of dicts, each describing one boolean copy toggle.
|
||||||
|
Keys:
|
||||||
|
name: (str, required)
|
||||||
|
label: (str, optional)
|
||||||
|
help_text: (str, optional)
|
||||||
|
default: (bool, optional, default True)
|
||||||
|
"""
|
||||||
|
# Enforce certain properties onto this serializer
|
||||||
|
kwargs['label'] = kwargs.get('label', _('Duplication Options'))
|
||||||
|
kwargs['help_text'] = kwargs.get(
|
||||||
|
'help_text', _('Specify options for duplicating this item')
|
||||||
|
)
|
||||||
|
kwargs['required'] = False
|
||||||
|
kwargs['write_only'] = True
|
||||||
|
|
||||||
|
copy_fields = copy_fields or []
|
||||||
|
copy_field_names = [spec['name'] for spec in copy_fields]
|
||||||
|
|
||||||
|
# Apply "default" fields
|
||||||
|
for name, label, help_text, default_value in self.DEFAULT_FIELDS:
|
||||||
|
popped_value = kwargs.pop(name, default_value)
|
||||||
|
|
||||||
|
if name in copy_field_names:
|
||||||
|
# Manually supplied field, continue
|
||||||
|
continue
|
||||||
|
|
||||||
|
if popped_value:
|
||||||
|
copy_fields.append({
|
||||||
|
'name': name,
|
||||||
|
'label': label,
|
||||||
|
'help_text': help_text,
|
||||||
|
'default': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Re-class the instance with a model-specific subclass,
|
||||||
|
# so that each model generates a unique schema component name
|
||||||
|
if self.__class__ is DuplicateOptionsSerializer:
|
||||||
|
self.__class__ = type(
|
||||||
|
f'{queryset.model.__name__}DuplicateOptionsSerializer',
|
||||||
|
(DuplicateOptionsSerializer,),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['original'] = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=queryset,
|
||||||
|
required=True,
|
||||||
|
label=_('Original'),
|
||||||
|
help_text=_('Select instance to duplicate'),
|
||||||
|
)
|
||||||
|
|
||||||
|
for spec in copy_fields or []:
|
||||||
|
self.fields[spec['name']] = serializers.BooleanField(
|
||||||
|
required=False,
|
||||||
|
default=spec.get('default', True),
|
||||||
|
label=spec.get('label', spec['name']),
|
||||||
|
help_text=spec.get('help_text', ''),
|
||||||
|
)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
|||||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
CustomStatusSerializerMixin,
|
CustomStatusSerializerMixin,
|
||||||
|
DuplicateOptionsSerializer,
|
||||||
FilterableSerializerMixin,
|
FilterableSerializerMixin,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
@@ -67,6 +68,8 @@ class BuildSerializer(
|
|||||||
):
|
):
|
||||||
"""Serializes a Build object."""
|
"""Serializes a Build object."""
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Serializer metaclass."""
|
"""Serializer metaclass."""
|
||||||
|
|
||||||
@@ -80,6 +83,7 @@ class BuildSerializer(
|
|||||||
'completed',
|
'completed',
|
||||||
'completion_date',
|
'completion_date',
|
||||||
'destination',
|
'destination',
|
||||||
|
'duplicate',
|
||||||
'external',
|
'external',
|
||||||
'parent',
|
'parent',
|
||||||
'part',
|
'part',
|
||||||
@@ -190,12 +194,29 @@ class BuildSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
duplicate = DuplicateOptionsSerializer(Build.objects.all(), copy_parameters=True)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Determine if extra serializer fields are required."""
|
"""Determine if extra serializer fields are required."""
|
||||||
kwargs.pop('create', False)
|
kwargs.pop('create', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""Create a new Build instance, optionally copying data from an existing build."""
|
||||||
|
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
|
||||||
|
|
||||||
def validate_reference(self, reference):
|
def validate_reference(self, reference):
|
||||||
"""Custom validation for the Build reference field."""
|
"""Custom validation for the Build reference field."""
|
||||||
# Ensure the reference matches the required pattern
|
# Ensure the reference matches the required pattern
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""JSON serializers for Company app."""
|
"""JSON serializers for Company app."""
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.ready import isGeneratingSchema
|
from InvenTree.ready import isGeneratingSchema
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
|
DuplicateOptionsSerializer,
|
||||||
FilterableSerializerMixin,
|
FilterableSerializerMixin,
|
||||||
InvenTreeCurrencySerializer,
|
InvenTreeCurrencySerializer,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
@@ -118,6 +120,8 @@ class CompanySerializer(
|
|||||||
|
|
||||||
import_exclude_fields = ['image']
|
import_exclude_fields = ['image']
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
@@ -132,6 +136,7 @@ class CompanySerializer(
|
|||||||
'email',
|
'email',
|
||||||
'currency',
|
'currency',
|
||||||
'contact',
|
'contact',
|
||||||
|
'duplicate',
|
||||||
'link',
|
'link',
|
||||||
'image',
|
'image',
|
||||||
'active',
|
'active',
|
||||||
@@ -191,6 +196,23 @@ class CompanySerializer(
|
|||||||
|
|
||||||
parameters = common.filters.enable_parameters_filter()
|
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()
|
@register_importer()
|
||||||
class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
||||||
@@ -217,6 +239,8 @@ class ManufacturerPartSerializer(
|
|||||||
):
|
):
|
||||||
"""Serializer for ManufacturerPart object."""
|
"""Serializer for ManufacturerPart object."""
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
@@ -229,6 +253,7 @@ class ManufacturerPartSerializer(
|
|||||||
'manufacturer',
|
'manufacturer',
|
||||||
'manufacturer_detail',
|
'manufacturer_detail',
|
||||||
'description',
|
'description',
|
||||||
|
'duplicate',
|
||||||
'MPN',
|
'MPN',
|
||||||
'link',
|
'link',
|
||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
@@ -241,6 +266,25 @@ class ManufacturerPartSerializer(
|
|||||||
|
|
||||||
parameters = common.filters.enable_parameters_filter()
|
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(
|
part_detail = OptionalField(
|
||||||
serializer_class=part_serializers.PartBriefSerializer,
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
serializer_kwargs={
|
serializer_kwargs={
|
||||||
@@ -323,6 +367,8 @@ class SupplierPartSerializer(
|
|||||||
|
|
||||||
export_exclude_fields = ['tags']
|
export_exclude_fields = ['tags']
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
export_child_fields = [
|
export_child_fields = [
|
||||||
'part_detail.name',
|
'part_detail.name',
|
||||||
'part_detail.description',
|
'part_detail.description',
|
||||||
@@ -339,6 +385,7 @@ class SupplierPartSerializer(
|
|||||||
'available',
|
'available',
|
||||||
'availability_updated',
|
'availability_updated',
|
||||||
'description',
|
'description',
|
||||||
|
'duplicate',
|
||||||
'in_stock',
|
'in_stock',
|
||||||
'on_order',
|
'on_order',
|
||||||
'link',
|
'link',
|
||||||
@@ -494,6 +541,10 @@ class SupplierPartSerializer(
|
|||||||
# Date fields
|
# Date fields
|
||||||
updated = serializers.DateTimeField(allow_null=True, read_only=True)
|
updated = serializers.DateTimeField(allow_null=True, read_only=True)
|
||||||
|
|
||||||
|
duplicate = DuplicateOptionsSerializer(
|
||||||
|
SupplierPart.objects.all(), copy_parameters=True
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Annotate the SupplierPart queryset with extra fields.
|
"""Annotate the SupplierPart queryset with extra fields.
|
||||||
@@ -522,8 +573,11 @@ class SupplierPartSerializer(
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Extract manufacturer data and process ManufacturerPart."""
|
"""Extract manufacturer data and process ManufacturerPart."""
|
||||||
|
duplicate = validated_data.pop('duplicate', None)
|
||||||
|
|
||||||
# Extract 'available' quantity from the serializer
|
# Extract 'available' quantity from the serializer
|
||||||
available = validated_data.pop('available', None)
|
available = validated_data.pop('available', None)
|
||||||
|
|
||||||
@@ -541,6 +595,12 @@ class SupplierPartSerializer(
|
|||||||
kwargs = {'manufacturer': manufacturer, 'MPN': MPN}
|
kwargs = {'manufacturer': manufacturer, 'MPN': MPN}
|
||||||
supplier_part.save(**kwargs)
|
supplier_part.save(**kwargs)
|
||||||
|
|
||||||
|
if duplicate:
|
||||||
|
original = duplicate['original']
|
||||||
|
|
||||||
|
if duplicate.get('copy_parameters', True):
|
||||||
|
supplier_part.copy_parameters_from(original)
|
||||||
|
|
||||||
return supplier_part
|
return supplier_part
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from taggit.serializers import TagListSerializerField
|
|||||||
import data_exporter.serializers
|
import data_exporter.serializers
|
||||||
import data_exporter.tasks
|
import data_exporter.tasks
|
||||||
import InvenTree.exceptions
|
import InvenTree.exceptions
|
||||||
|
import InvenTree.serializers
|
||||||
from common.models import DataOutput
|
from common.models import DataOutput
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
@@ -64,6 +65,14 @@ class DataExportSerializerMixin:
|
|||||||
# Exclude fields which are not required for data export
|
# Exclude fields which are not required for data export
|
||||||
for field in self.get_export_exclude_fields(**kwargs):
|
for field in self.get_export_exclude_fields(**kwargs):
|
||||||
self.fields.pop(field, None)
|
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:
|
else:
|
||||||
# Exclude fields which are only used for data export
|
# Exclude fields which are only used for data export
|
||||||
for field in self.get_export_only_fields(**kwargs):
|
for field in self.get_export_only_fields(**kwargs):
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
from rest_framework import fields, serializers
|
from rest_framework import fields, serializers
|
||||||
from taggit.serializers import TagListSerializerField
|
from taggit.serializers import TagListSerializerField
|
||||||
|
|
||||||
|
import InvenTree.serializers
|
||||||
|
|
||||||
|
|
||||||
class DataImportSerializerMixin:
|
class DataImportSerializerMixin:
|
||||||
"""Mixin class for adding data import functionality to a DRF serializer."""
|
"""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):
|
for field in self.get_import_exclude_fields(**kwargs):
|
||||||
self.fields.pop(field, None)
|
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:
|
else:
|
||||||
# Exclude fields which are only used for data import
|
# Exclude fields which are only used for data import
|
||||||
for field in self.get_import_only_fields(**kwargs):
|
for field in self.get_import_only_fields(**kwargs):
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -110,9 +77,9 @@ class AbstractOrderSerializer(
|
|||||||
):
|
):
|
||||||
"""Abstract serializer class which provides fields common to all order types."""
|
"""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
|
# Number of line items in this order
|
||||||
line_items = serializers.IntegerField(
|
line_items = serializers.IntegerField(
|
||||||
@@ -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.
|
||||||
@@ -1411,6 +1379,8 @@ class SalesOrderShipmentSerializer(
|
|||||||
):
|
):
|
||||||
"""Serializer for the SalesOrderShipment class."""
|
"""Serializer for the SalesOrderShipment class."""
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
@@ -1423,6 +1393,7 @@ class SalesOrderShipmentSerializer(
|
|||||||
'shipment_address',
|
'shipment_address',
|
||||||
'delivery_date',
|
'delivery_date',
|
||||||
'checked_by',
|
'checked_by',
|
||||||
|
'duplicate',
|
||||||
'reference',
|
'reference',
|
||||||
'tracking_number',
|
'tracking_number',
|
||||||
'invoice_number',
|
'invoice_number',
|
||||||
@@ -1507,6 +1478,25 @@ class SalesOrderShipmentSerializer(
|
|||||||
|
|
||||||
tags = common.filters.enable_tags_filter()
|
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(
|
class SalesOrderAllocationSerializer(
|
||||||
FilterableSerializerMixin, InvenTreeModelSerializer
|
FilterableSerializerMixin, InvenTreeModelSerializer
|
||||||
@@ -2198,6 +2188,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 +2483,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.
|
||||||
|
|||||||
@@ -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},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
@@ -601,7 +540,7 @@ class PartSerializer(
|
|||||||
Used when displaying all details of a single component.
|
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:
|
class Meta:
|
||||||
"""Metaclass defining serializer fields."""
|
"""Metaclass defining serializer fields."""
|
||||||
@@ -1007,11 +946,37 @@ 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_parameters=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_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 +1042,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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -36,16 +36,18 @@ import {
|
|||||||
} from '../hooks/UseGenerator';
|
} from '../hooks/UseGenerator';
|
||||||
import { useGlobalSettingsState } from '../states/SettingsStates';
|
import { useGlobalSettingsState } from '../states/SettingsStates';
|
||||||
import { RenderPartColumn } from '../tables/ColumnRenderers';
|
import { RenderPartColumn } from '../tables/ColumnRenderers';
|
||||||
import { ProjectCodeField, TagsField } from './CommonFields';
|
import { DuplicateField, ProjectCodeField, TagsField } from './CommonFields';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field set for BuildOrder forms
|
* Field set for BuildOrder forms
|
||||||
*/
|
*/
|
||||||
export function useBuildOrderFields({
|
export function useBuildOrderFields({
|
||||||
create,
|
create,
|
||||||
|
duplicateBuildId,
|
||||||
modalId
|
modalId
|
||||||
}: {
|
}: {
|
||||||
create: boolean;
|
create: boolean;
|
||||||
|
duplicateBuildId?: number | null;
|
||||||
modalId: string;
|
modalId: string;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
const [destination, setDestination] = useState<number | null | undefined>(
|
const [destination, setDestination] = useState<number | null | undefined>(
|
||||||
@@ -133,7 +135,13 @@ export function useBuildOrderFields({
|
|||||||
is_active: true
|
is_active: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
external: {}
|
external: {},
|
||||||
|
duplicate: DuplicateField({
|
||||||
|
originalId: duplicateBuildId,
|
||||||
|
extraFields: {
|
||||||
|
copy_parameters: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) {
|
if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) {
|
||||||
@@ -144,8 +152,19 @@ export function useBuildOrderFields({
|
|||||||
delete fields.external;
|
delete fields.external;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!duplicateBuildId) {
|
||||||
|
delete fields.duplicate;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [create, destination, batchCode, batchGenerator.result, globalSettings]);
|
}, [
|
||||||
|
create,
|
||||||
|
destination,
|
||||||
|
batchCode,
|
||||||
|
batchGenerator.result,
|
||||||
|
globalSettings,
|
||||||
|
duplicateBuildId
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBuildOrderOutputFields({
|
export function useBuildOrderOutputFields({
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ import type { ApiFormFieldType } from '@lib/types/Forms';
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { IconList } from '@tabler/icons-react';
|
import { IconList } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
// Generic field for implementing a "duplication options" form field
|
||||||
|
export function DuplicateField({
|
||||||
|
originalId,
|
||||||
|
extraFields
|
||||||
|
}: Readonly<{
|
||||||
|
originalId?: number | null;
|
||||||
|
extraFields?: Record<string, any>;
|
||||||
|
}>): ApiFormFieldType {
|
||||||
|
return {
|
||||||
|
children: {
|
||||||
|
original: {
|
||||||
|
value: originalId,
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
...extraFields
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic field for rendering a list of tags within a form
|
||||||
export function TagsField({
|
export function TagsField({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
IconPhone
|
IconPhone
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { TagsField } from './CommonFields';
|
import { DuplicateField, TagsField } from './CommonFields';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field set for SupplierPart instance
|
* Field set for SupplierPart instance
|
||||||
@@ -21,11 +21,13 @@ import { TagsField } from './CommonFields';
|
|||||||
export function useSupplierPartFields({
|
export function useSupplierPartFields({
|
||||||
manufacturerId,
|
manufacturerId,
|
||||||
manufacturerPartId,
|
manufacturerPartId,
|
||||||
partId
|
partId,
|
||||||
|
duplicateSupplierPartId
|
||||||
}: {
|
}: {
|
||||||
manufacturerId?: number;
|
manufacturerId?: number;
|
||||||
manufacturerPartId?: number;
|
manufacturerPartId?: number;
|
||||||
partId?: number;
|
partId?: number;
|
||||||
|
duplicateSupplierPartId?: number | null;
|
||||||
}) {
|
}) {
|
||||||
const [part, setPart] = useState<any>({});
|
const [part, setPart] = useState<any>({});
|
||||||
|
|
||||||
@@ -95,14 +97,34 @@ export function useSupplierPartFields({
|
|||||||
icon: <IconPackage />
|
icon: <IconPackage />
|
||||||
},
|
},
|
||||||
primary: {},
|
primary: {},
|
||||||
active: {}
|
active: {},
|
||||||
|
duplicate: DuplicateField({
|
||||||
|
originalId: duplicateSupplierPartId,
|
||||||
|
extraFields: {
|
||||||
|
copy_parameters: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!duplicateSupplierPartId) {
|
||||||
|
delete fields.duplicate;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [manufacturerId, manufacturerPartId, partId, part]);
|
}, [
|
||||||
|
manufacturerId,
|
||||||
|
manufacturerPartId,
|
||||||
|
partId,
|
||||||
|
part,
|
||||||
|
duplicateSupplierPartId
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useManufacturerPartFields() {
|
export function useManufacturerPartFields({
|
||||||
|
duplicateManufacturerPartId
|
||||||
|
}: {
|
||||||
|
duplicateManufacturerPartId?: number | null;
|
||||||
|
} = {}) {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
part: {},
|
part: {},
|
||||||
@@ -120,18 +142,32 @@ export function useManufacturerPartFields() {
|
|||||||
MPN: {},
|
MPN: {},
|
||||||
description: {},
|
description: {},
|
||||||
tags: TagsField({}),
|
tags: TagsField({}),
|
||||||
link: {}
|
link: {},
|
||||||
|
duplicate: DuplicateField({
|
||||||
|
originalId: duplicateManufacturerPartId,
|
||||||
|
extraFields: {
|
||||||
|
copy_parameters: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!duplicateManufacturerPartId) {
|
||||||
|
delete fields.duplicate;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, []);
|
}, [duplicateManufacturerPartId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field set for editing a company instance
|
* Field set for editing a company instance
|
||||||
*/
|
*/
|
||||||
export function companyFields(): ApiFormFieldSet {
|
export function companyFields({
|
||||||
return {
|
duplicateCompanyId
|
||||||
|
}: {
|
||||||
|
duplicateCompanyId?: number | null;
|
||||||
|
} = {}): ApiFormFieldSet {
|
||||||
|
const fields: ApiFormFieldSet = {
|
||||||
name: {},
|
name: {},
|
||||||
description: {},
|
description: {},
|
||||||
website: {
|
website: {
|
||||||
@@ -151,6 +187,18 @@ export function companyFields(): ApiFormFieldSet {
|
|||||||
is_supplier: {},
|
is_supplier: {},
|
||||||
is_manufacturer: {},
|
is_manufacturer: {},
|
||||||
is_customer: {},
|
is_customer: {},
|
||||||
active: {}
|
active: {},
|
||||||
|
duplicate: DuplicateField({
|
||||||
|
originalId: duplicateCompanyId,
|
||||||
|
extraFields: {
|
||||||
|
copy_parameters: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!duplicateCompanyId) {
|
||||||
|
delete fields.duplicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -632,6 +632,7 @@ export default function BuildDetail() {
|
|||||||
|
|
||||||
const duplicateBuildOrderFields = useBuildOrderFields({
|
const duplicateBuildOrderFields = useBuildOrderFields({
|
||||||
create: false,
|
create: false,
|
||||||
|
duplicateBuildId: build.pk,
|
||||||
modalId: 'duplicate-build-order'
|
modalId: 'duplicate-build-order'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { DetailsImage } from '../../components/details/DetailsImage';
|
|||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
|
DuplicateItemAction,
|
||||||
EditItemAction,
|
EditItemAction,
|
||||||
OptionsActionDropdown
|
OptionsActionDropdown
|
||||||
} from '../../components/items/ActionDropdown';
|
} from '../../components/items/ActionDropdown';
|
||||||
@@ -43,6 +44,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
|
|||||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||||
import { companyFields } from '../../forms/CompanyForms';
|
import { companyFields } from '../../forms/CompanyForms';
|
||||||
import {
|
import {
|
||||||
|
useCreateApiFormModal,
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
@@ -293,11 +295,23 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
url: ApiEndpoints.company_list,
|
url: ApiEndpoints.company_list,
|
||||||
pk: company?.pk,
|
pk: company?.pk,
|
||||||
title: t`Edit Company`,
|
title: t`Edit Company`,
|
||||||
fields: companyFields(),
|
fields: useMemo(() => companyFields({}), []),
|
||||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||||
onFormSuccess: refreshInstance
|
onFormSuccess: refreshInstance
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const duplicateCompany = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.company_list,
|
||||||
|
title: t`Duplicate Company`,
|
||||||
|
initialData: useMemo(() => ({ ...company }), [company]),
|
||||||
|
fields: useMemo(
|
||||||
|
() => companyFields({ duplicateCompanyId: company?.pk }),
|
||||||
|
[company]
|
||||||
|
),
|
||||||
|
follow: true,
|
||||||
|
modelType: ModelType.company
|
||||||
|
});
|
||||||
|
|
||||||
const deleteCompany = useDeleteApiFormModal({
|
const deleteCompany = useDeleteApiFormModal({
|
||||||
url: ApiEndpoints.company_list,
|
url: ApiEndpoints.company_list,
|
||||||
pk: company?.pk,
|
pk: company?.pk,
|
||||||
@@ -322,6 +336,10 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
hidden: !user.hasChangeRole(UserRoles.purchase_order),
|
hidden: !user.hasChangeRole(UserRoles.purchase_order),
|
||||||
onClick: () => editCompany.open()
|
onClick: () => editCompany.open()
|
||||||
}),
|
}),
|
||||||
|
DuplicateItemAction({
|
||||||
|
hidden: !user.hasAddRole(UserRoles.purchase_order),
|
||||||
|
onClick: () => duplicateCompany.open()
|
||||||
|
}),
|
||||||
DeleteItemAction({
|
DeleteItemAction({
|
||||||
hidden: !user.hasDeleteRole(UserRoles.purchase_order),
|
hidden: !user.hasDeleteRole(UserRoles.purchase_order),
|
||||||
onClick: () => deleteCompany.open()
|
onClick: () => deleteCompany.open()
|
||||||
@@ -345,6 +363,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
<>
|
<>
|
||||||
{editCompany.modal}
|
{editCompany.modal}
|
||||||
{deleteCompany.modal}
|
{deleteCompany.modal}
|
||||||
|
{duplicateCompany.modal}
|
||||||
<InstanceDetail
|
<InstanceDetail
|
||||||
query={instanceQuery}
|
query={instanceQuery}
|
||||||
requiredPermission={ModelType.company}
|
requiredPermission={ModelType.company}
|
||||||
|
|||||||
@@ -220,10 +220,14 @@ export default function ManufacturerPartDetail() {
|
|||||||
onFormSuccess: refreshInstance
|
onFormSuccess: refreshInstance
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const duplicateManufacturerPartFields = useManufacturerPartFields({
|
||||||
|
duplicateManufacturerPartId: manufacturerPart?.pk
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateManufacturerPart = useCreateApiFormModal({
|
const duplicateManufacturerPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.manufacturer_part_list,
|
url: ApiEndpoints.manufacturer_part_list,
|
||||||
title: t`Add Manufacturer Part`,
|
title: t`Add Manufacturer Part`,
|
||||||
fields: editManufacturerPartFields,
|
fields: duplicateManufacturerPartFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
...manufacturerPart
|
...manufacturerPart
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -357,10 +357,14 @@ export default function SupplierPartDetail() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const duplicateSupplierPartFields = useSupplierPartFields({
|
||||||
|
duplicateSupplierPartId: supplierPart?.pk
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateSupplierPart = useCreateApiFormModal({
|
const duplicateSupplierPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.supplier_part_list,
|
url: ApiEndpoints.supplier_part_list,
|
||||||
title: t`Add Supplier Part`,
|
title: t`Add Supplier Part`,
|
||||||
fields: supplierPartFields,
|
fields: duplicateSupplierPartFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
...supplierPart
|
...supplierPart
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ActionDropdown } from '../../components/items/ActionDropdown';
|
|||||||
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
|
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||||
|
import { DuplicateField } from '../../forms/CommonFields';
|
||||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||||
import { usePartFields } from '../../forms/PartForms';
|
import { usePartFields } from '../../forms/PartForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
@@ -312,12 +313,9 @@ export function PartListTable({
|
|||||||
const duplicatePartFields: ApiFormFieldSet = useMemo(() => {
|
const duplicatePartFields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...createPartFields,
|
...createPartFields,
|
||||||
duplicate: {
|
duplicate: DuplicateField({
|
||||||
children: {
|
originalId: selectedPart.pk,
|
||||||
part: {
|
extraFields: {
|
||||||
value: selectedPart.pk,
|
|
||||||
hidden: true
|
|
||||||
},
|
|
||||||
copy_image: {
|
copy_image: {
|
||||||
value: true
|
value: true
|
||||||
},
|
},
|
||||||
@@ -337,7 +335,7 @@ export function PartListTable({
|
|||||||
hidden: !selectedPart.testable
|
hidden: !selectedPart.testable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
}, [createPartFields, globalSettings, selectedPart]);
|
}, [createPartFields, globalSettings, selectedPart]);
|
||||||
|
|
||||||
|
|||||||
@@ -133,10 +133,14 @@ export function ManufacturerPartTable({
|
|||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const duplicateManufacturerPartFields = useManufacturerPartFields({
|
||||||
|
duplicateManufacturerPartId: selectedPart?.pk
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateManufacturerPart = useCreateApiFormModal({
|
const duplicateManufacturerPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.manufacturer_part_list,
|
url: ApiEndpoints.manufacturer_part_list,
|
||||||
title: t`Add Manufacturer Part`,
|
title: t`Add Manufacturer Part`,
|
||||||
fields: useMemo(() => manufacturerPartFields, [manufacturerPartFields]),
|
fields: duplicateManufacturerPartFields,
|
||||||
table: table,
|
table: table,
|
||||||
initialData: {
|
initialData: {
|
||||||
...selectedPart
|
...selectedPart
|
||||||
|
|||||||
@@ -292,10 +292,14 @@ export function SupplierPartTable({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const duplicateSupplierPartFields = useSupplierPartFields({
|
||||||
|
duplicateSupplierPartId: selectedSupplierPart?.pk
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateSupplierPart = useCreateApiFormModal({
|
const duplicateSupplierPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.supplier_part_list,
|
url: ApiEndpoints.supplier_part_list,
|
||||||
title: t`Add Supplier Part`,
|
title: t`Add Supplier Part`,
|
||||||
fields: useMemo(() => editSupplierPartFields, [editSupplierPartFields]),
|
fields: duplicateSupplierPartFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
...selectedSupplierPart,
|
...selectedSupplierPart,
|
||||||
primary: false,
|
primary: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user