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

Part units (#4854)

* Add validation to part units field

* Add "pack_units" field to the SupplierPart model

* Migrate old units to new units, and remove old field

* Table fix

* Fixture fix

* Update migration

* Improve "hook" for loading custom unit database

* Display part units column in part table

- Also allow ordering by part units
- Allow filtering to show parts which have defined units

* Adds data migration for converting units to valid values

* Add "pack_units_native" field to company.SupplierPart model

* Clean pack units when saving a SupplierPart

- Convert to native part units
- Handle empty units value
- Add unit tests

* Add background function to rebuild supplier parts when a part is saved

- Required to ensure that the "pack_size_native" is up to date

* Template updates

* Sort by native units first

* Bump API version

* Rename "pack_units" to "pack_quantity"

* Update migration file

- Allow reverse migration

* Fix for currency migration

- Handle case where no currencies are provided
- Handle case where base currency is not in provided options

* Adds unit test for data migration

* Add unit test for part.units data migration

- Check that units fields are updated correctly

* Add some extra "default units"

- each / piece
- dozen / hundred / thousand
- Add unit testing also

* Update references to "pack_size"

- Replace with "pack_quantity" or "pack_quantity_native" as appropriate

* Improvements based on unit testing

* catch error

* Docs updates

* Fixes for pricing tests

* Update unit tests for part migrations · 1b6b6d9d

* Bug fix for conversion code

* javascript updates

* JS formatting fix
This commit is contained in:
Oliver
2023-05-26 16:57:23 +10:00
committed by GitHub
parent 717bb07dcf
commit 5dd6f18495
39 changed files with 878 additions and 251 deletions

View File

@ -20,21 +20,11 @@ from taggit.serializers import TagListSerializerField
import common.models
import company.models
import InvenTree.helpers
import InvenTree.serializers
import InvenTree.status
import part.filters
import part.tasks
import stock.models
from InvenTree.serializers import (DataFileExtractSerializer,
DataFileUploadSerializer,
InvenTreeAttachmentSerializer,
InvenTreeAttachmentSerializerField,
InvenTreeCurrencySerializer,
InvenTreeDecimalField,
InvenTreeImageSerializerField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer,
InvenTreeTagModelSerializer,
RemoteImageMixin, UserSerializer)
from InvenTree.status_codes import BuildStatus
from InvenTree.tasks import offload_task
@ -48,7 +38,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
logger = logging.getLogger("inventree")
class CategorySerializer(InvenTreeModelSerializer):
class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for PartCategory."""
class Meta:
@ -94,7 +84,7 @@ class CategorySerializer(InvenTreeModelSerializer):
starred = serializers.SerializerMethodField()
class CategoryTree(InvenTreeModelSerializer):
class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for PartCategory tree."""
class Meta:
@ -108,19 +98,19 @@ class CategoryTree(InvenTreeModelSerializer):
]
class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
class PartAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
"""Serializer for the PartAttachment class."""
class Meta:
"""Metaclass defining serializer fields"""
model = PartAttachment
fields = InvenTreeAttachmentSerializer.attachment_fields([
fields = InvenTree.serializers.InvenTreeAttachmentSerializer.attachment_fields([
'part',
])
class PartTestTemplateSerializer(InvenTreeModelSerializer):
class PartTestTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartTestTemplate class."""
class Meta:
@ -141,7 +131,7 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer):
key = serializers.CharField(read_only=True)
class PartSalePriceSerializer(InvenTreeModelSerializer):
class PartSalePriceSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for sale prices for Part model."""
class Meta:
@ -155,14 +145,14 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
'price_currency',
]
quantity = InvenTreeDecimalField()
quantity = InvenTree.serializers.InvenTreeDecimalField()
price = InvenTreeMoneySerializer(allow_null=True)
price = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True)
price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
price_currency = InvenTree.serializers.InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
class PartInternalPriceSerializer(InvenTreeModelSerializer):
class PartInternalPriceSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for internal prices for Part model."""
class Meta:
@ -176,13 +166,13 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
'price_currency',
]
quantity = InvenTreeDecimalField()
quantity = InvenTree.serializers.InvenTreeDecimalField()
price = InvenTreeMoneySerializer(
price = InvenTree.serializers.InvenTreeMoneySerializer(
allow_null=True
)
price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
price_currency = InvenTree.serializers.InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
class PartThumbSerializer(serializers.Serializer):
@ -195,7 +185,7 @@ class PartThumbSerializer(serializers.Serializer):
count = serializers.IntegerField(read_only=True)
class PartThumbSerializerUpdate(InvenTreeModelSerializer):
class PartThumbSerializerUpdate(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for updating Part thumbnail."""
class Meta:
@ -212,10 +202,10 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer):
raise serializers.ValidationError("File is not an image")
return value
image = InvenTreeAttachmentSerializerField(required=True)
image = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True)
class PartParameterTemplateSerializer(InvenTreeModelSerializer):
class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""JSON serializer for the PartParameterTemplate model."""
class Meta:
@ -229,7 +219,7 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
]
class PartParameterSerializer(InvenTreeModelSerializer):
class PartParameterSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""JSON serializers for the PartParameter model."""
class Meta:
@ -260,7 +250,7 @@ class PartParameterSerializer(InvenTreeModelSerializer):
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
class PartBriefSerializer(InvenTreeModelSerializer):
class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for Part (brief detail)"""
class Meta:
@ -295,8 +285,8 @@ class PartBriefSerializer(InvenTreeModelSerializer):
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
# Pricing fields
pricing_min = InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True)
pricing_max = InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True)
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
class DuplicatePartSerializer(serializers.Serializer):
@ -406,7 +396,7 @@ class InitialSupplierSerializer(serializers.Serializer):
return data
class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serializers.InvenTreeTagModelSerializer):
"""Serializer for complete detail information of a part.
Used when displaying all details of a single component.
@ -607,7 +597,7 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
stock_item_count = serializers.IntegerField(read_only=True)
suppliers = serializers.IntegerField(read_only=True)
image = InvenTreeImageSerializerField(required=False, allow_null=True)
image = InvenTree.serializers.InvenTreeImageSerializerField(required=False, allow_null=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField()
@ -615,8 +605,8 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all())
# Pricing fields
pricing_min = InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True)
pricing_max = InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True)
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
parameters = PartParameterSerializer(
many=True,
@ -771,7 +761,7 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
return self.instance
class PartStocktakeSerializer(InvenTreeModelSerializer):
class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartStocktake model"""
class Meta:
@ -800,13 +790,13 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField()
user_detail = UserSerializer(source='user', read_only=True, many=False)
user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True, many=False)
cost_min = InvenTreeMoneySerializer(allow_null=True)
cost_min_currency = InvenTreeCurrencySerializer()
cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True)
cost_min_currency = InvenTree.serializers.InvenTreeCurrencySerializer()
cost_max = InvenTreeMoneySerializer(allow_null=True)
cost_max_currency = InvenTreeCurrencySerializer()
cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True)
cost_max_currency = InvenTree.serializers.InvenTreeCurrencySerializer()
def save(self):
"""Called when this serializer is saved"""
@ -820,7 +810,7 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
super().save()
class PartStocktakeReportSerializer(InvenTreeModelSerializer):
class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for stocktake report class"""
class Meta:
@ -836,9 +826,9 @@ class PartStocktakeReportSerializer(InvenTreeModelSerializer):
'user_detail',
]
user_detail = UserSerializer(source='user', read_only=True, many=False)
user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True, many=False)
report = InvenTreeAttachmentSerializerField(read_only=True)
report = InvenTree.serializers.InvenTreeAttachmentSerializerField(read_only=True)
class PartStocktakeReportGenerateSerializer(serializers.Serializer):
@ -906,7 +896,7 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
)
class PartPricingSerializer(InvenTreeModelSerializer):
class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for Part pricing information"""
class Meta:
@ -942,29 +932,29 @@ class PartPricingSerializer(InvenTreeModelSerializer):
scheduled_for_update = serializers.BooleanField(read_only=True)
# Custom serializers
bom_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
bom_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
bom_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
bom_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
purchase_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
purchase_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
purchase_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
purchase_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
internal_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
internal_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
internal_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
internal_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
supplier_price_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
supplier_price_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
supplier_price_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
supplier_price_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
variant_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
variant_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
variant_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
variant_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
overall_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
overall_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
overall_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
overall_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_price_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_price_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_price_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_price_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_history_min = InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_history_max = InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_history_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_history_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
update = serializers.BooleanField(
write_only=True,
@ -984,7 +974,7 @@ class PartPricingSerializer(InvenTreeModelSerializer):
pricing.update_pricing()
class PartRelationSerializer(InvenTreeModelSerializer):
class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for a PartRelated model."""
class Meta:
@ -1002,7 +992,7 @@ class PartRelationSerializer(InvenTreeModelSerializer):
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)
class PartStarSerializer(InvenTreeModelSerializer):
class PartStarSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for a PartStar object."""
class Meta:
@ -1020,7 +1010,7 @@ class PartStarSerializer(InvenTreeModelSerializer):
username = serializers.CharField(source='user.username', read_only=True)
class BomItemSubstituteSerializer(InvenTreeModelSerializer):
class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the BomItemSubstitute class."""
class Meta:
@ -1036,7 +1026,7 @@ class BomItemSubstituteSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', read_only=True, many=False)
class BomItemSerializer(InvenTreeModelSerializer):
class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for BomItem object."""
class Meta:
@ -1087,7 +1077,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
if sub_part_detail is not True:
self.fields.pop('sub_part_detail')
quantity = InvenTreeDecimalField(required=True)
quantity = InvenTree.serializers.InvenTreeDecimalField(required=True)
def validate_quantity(self, quantity):
"""Perform validation for the BomItem quantity field"""
@ -1109,8 +1099,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
on_order = serializers.FloatField(read_only=True)
# Cached pricing fields
pricing_min = InvenTreeMoneySerializer(source='sub_part.pricing.overall_min', allow_null=True, read_only=True)
pricing_max = InvenTreeMoneySerializer(source='sub_part.pricing.overall_max', allow_null=True, read_only=True)
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(source='sub_part.pricing.overall_min', allow_null=True, read_only=True)
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='sub_part.pricing.overall_max', allow_null=True, read_only=True)
# Annotated fields for available stock
available_stock = serializers.FloatField(read_only=True)
@ -1212,7 +1202,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
return queryset
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
class CategoryParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartCategoryParameterTemplate model."""
class Meta:
@ -1297,7 +1287,7 @@ class PartCopyBOMSerializer(serializers.Serializer):
)
class BomImportUploadSerializer(DataFileUploadSerializer):
class BomImportUploadSerializer(InvenTree.serializers.DataFileUploadSerializer):
"""Serializer for uploading a file and extracting data from it."""
TARGET_MODEL = BomItem
@ -1333,7 +1323,7 @@ class BomImportUploadSerializer(DataFileUploadSerializer):
part.bom_items.all().delete()
class BomImportExtractSerializer(DataFileExtractSerializer):
class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer):
"""Serializer class for exatracting BOM data from an uploaded file.
The parent class DataFileExtractSerializer does most of the heavy lifting here.