2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

Improvements for part creation API endpoint (#4281)

* Refactor javascript for creating a new part

* Simplify method of removing create fields from serializer

* Fix bug which resulted in multiple model instances being created

* remove custom code required on Part model

* Reorganize existing Part API test code

* Add child serializer for part duplication options

* Part duplication is now handled by the DRF serializer

- Improved validation options
- API is self-documenting (no more secret fields)
- More DRY

* Initial stock is now handled by the DRF serializer

* Adds child serializer for adding initial supplier data for a Part instance

* Create initial supplier and manufacturer parts as specified

* Adding unit tests

* Add unit tests for part duplication via API

* Bump API version

* Add javascript for automatically extracting info for nested fields

* Improvements for part creation form rendering

- Move to nested fields (using API metadata)
- Visual improvements
- Improve some field name / description values

* Properly format nested fields for sending to the server

* Handle error case for scrollIntoView

* Display errors for nested fields

* Fix bug for filling part category

* JS linting fixes

* Unit test fixes

* Fixes for unit tests

* Further fixes to unit tests
This commit is contained in:
Oliver
2023-02-02 09:24:16 +11:00
committed by GitHub
parent c6df0dbb2d
commit 4f029d4d81
15 changed files with 770 additions and 585 deletions

View File

@ -5,6 +5,7 @@ import io
from decimal import Decimal
from django.core.files.base import ContentFile
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import ExpressionWrapper, F, FloatField, Q
from django.db.models.functions import Coalesce
@ -14,8 +15,10 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from sql_util.utils import SubqueryCount, SubquerySum
import company.models
import InvenTree.helpers
import part.filters
import stock.models
from common.settings import currency_code_default, currency_code_mappings
from InvenTree.serializers import (DataFileExtractSerializer,
DataFileUploadSerializer,
@ -304,6 +307,113 @@ class PartBriefSerializer(InvenTreeModelSerializer):
]
class DuplicatePartSerializer(serializers.Serializer):
"""Serializer for specifying options when duplicating a Part.
The fields in this serializer control how the Part is duplicated.
"""
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,
)
class InitialStockSerializer(serializers.Serializer):
"""Serializer for creating initial stock quantity."""
quantity = serializers.DecimalField(
max_digits=15, decimal_places=5, validators=[MinValueValidator(0)],
label=_('Initial Stock Quantity'), help_text=_('Specify initial stock quantity for this Part. If quantity is zero, no stock is added.'),
required=True,
)
location = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockLocation.objects.all(),
label=_('Initial Stock Location'), help_text=_('Specify initial stock location for this Part'),
allow_null=True, required=False,
)
class InitialSupplierSerializer(serializers.Serializer):
"""Serializer for adding initial supplier / manufacturer information"""
supplier = serializers.PrimaryKeyRelatedField(
queryset=company.models.Company.objects.all(),
label=_('Supplier'), help_text=_('Select supplier (or leave blank to skip)'),
allow_null=True, required=False,
)
sku = serializers.CharField(
max_length=100, required=False, allow_blank=True,
label=_('SKU'), help_text=_('Supplier stock keeping unit'),
)
manufacturer = serializers.PrimaryKeyRelatedField(
queryset=company.models.Company.objects.all(),
label=_('Manufacturer'), help_text=_('Select manufacturer (or leave blank to skip)'),
allow_null=True, required=False,
)
mpn = serializers.CharField(
max_length=100, required=False, allow_blank=True,
label=_('MPN'), help_text=_('Manufacturer part number'),
)
def validate_supplier(self, company):
"""Validation for the provided Supplier"""
if company and not company.is_supplier:
raise serializers.ValidationError(_('Selected company is not a valid supplier'))
return company
def validate_manufacturer(self, company):
"""Validation for the provided Manufacturer"""
if company and not company.is_manufacturer:
raise serializers.ValidationError(_('Selected company is not a valid manufacturer'))
return company
def validate(self, data):
"""Extra validation for this serializer"""
if company.models.ManufacturerPart.objects.filter(
manufacturer=data.get('manufacturer', None),
MPN=data.get('mpn', '')
).exists():
raise serializers.ValidationError({
'mpn': _('Manufacturer part matching this MPN already exists')
})
if company.models.SupplierPart.objects.filter(
supplier=data.get('supplier', None),
SKU=data.get('sku', '')
).exists():
raise serializers.ValidationError({
'sku': _('Supplier part matching this SKU already exists')
})
return data
class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
"""Serializer for complete detail information of a part.
@ -314,6 +424,19 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
"""Return the API url associated with this serializer"""
return reverse_lazy('api-part-list')
def skip_create_fields(self):
"""Skip these fields when instantiating a new Part instance"""
fields = super().skip_create_fields()
fields += [
'duplicate',
'initial_stock',
'initial_supplier',
]
return fields
def __init__(self, *args, **kwargs):
"""Custom initialization method for PartSerializer:
@ -325,6 +448,8 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
parameters = kwargs.pop('parameters', False)
create = kwargs.pop('create', False)
super().__init__(*args, **kwargs)
if category_detail is not True:
@ -333,6 +458,11 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
if parameters is not True:
self.fields.pop('parameters')
if create is not True:
# These fields are only used for the LIST API endpoint
for f in self.skip_create_fields()[1:]:
self.fields.pop(f)
@staticmethod
def annotate_queryset(queryset):
"""Add some extra annotations to the queryset.
@ -427,6 +557,22 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
read_only=True,
)
# Extra fields used only for creation of a new Part instance
duplicate = DuplicatePartSerializer(
label=_('Duplicate Part'), help_text=_('Copy initial data from another Part'),
write_only=True, required=False
)
initial_stock = InitialStockSerializer(
label=_('Initial Stock'), help_text=_('Create Part with initial stock quantity'),
write_only=True, required=False,
)
initial_supplier = InitialSupplierSerializer(
label=_('Supplier Information'), help_text=_('Add initial supplier information for this part'),
write_only=True, required=False,
)
class Meta:
"""Metaclass defining serializer fields"""
model = Part
@ -475,12 +621,83 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
'virtual',
'pricing_min',
'pricing_max',
# Fields only used for Part creation
'duplicate',
'initial_stock',
'initial_supplier',
]
read_only_fields = [
'barcode_hash',
]
@transaction.atomic
def create(self, validated_data):
"""Custom method for creating a new Part instance using this serializer"""
duplicate = validated_data.pop('duplicate', None)
initial_stock = validated_data.pop('initial_stock', None)
initial_supplier = validated_data.pop('initial_supplier', None)
instance = super().create(validated_data)
# Copy data from original Part
if duplicate:
original = duplicate['part']
if duplicate['copy_bom']:
instance.copy_bom_from(original)
if duplicate['copy_image']:
instance.image = original.image
instance.save()
if duplicate['copy_parameters']:
instance.copy_parameters_from(original)
# Create initial stock entry
if initial_stock:
quantity = initial_stock['quantity']
location = initial_stock['location'] or instance.default_location
if quantity > 0:
stockitem = stock.models.StockItem(
part=instance,
quantity=quantity,
location=location,
)
stockitem.save(user=self.context['request'].user)
# Create initial supplier information
if initial_supplier:
manufacturer = initial_supplier.get('manufacturer', None)
mpn = initial_supplier.get('mpn', '')
if manufacturer and mpn:
manu_part = company.models.ManufacturerPart.objects.create(
part=instance,
manufacturer=manufacturer,
MPN=mpn
)
else:
manu_part = None
supplier = initial_supplier.get('supplier', None)
sku = initial_supplier.get('sku', '')
if supplier and sku:
company.models.SupplierPart.objects.create(
part=instance,
supplier=supplier,
SKU=sku,
manufacturer_part=manu_part,
)
return instance
def save(self):
"""Save the Part instance"""