mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[Refactoring] Data Export (#8950)
* Allow extraction of "child" fields when exporting serialized data * Update StockItemSerializer * Add missing default attribute * Cleanup export for BuildItemSerializer * Refactor BuildLineSerializer * Refactor BomItemSerializer * Auto-exclude tags from export (for now) * Cleanup SupplierPartSerializer * Updated unit test * Cleanup * Bump API version * Reduce serializer complexity * Refactor StockLocation API endpoints * Cleanup API * Enhanced docstrings
This commit is contained in:
parent
5968f5670f
commit
ddcb7980ff
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 304
|
INVENTREE_API_VERSION = 305
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v305 - 2025-01-26 : https://github.com/inventree/InvenTree/pull/8950
|
||||||
|
- Bug fixes for the SupplierPart API
|
||||||
|
- Refactoring for data export via API
|
||||||
|
|
||||||
v304 - 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940
|
v304 - 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940
|
||||||
- Adds "category" filter to build list API
|
- Adds "category" filter to build list API
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import QuerySet
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -820,26 +821,20 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f'items() method not implemented for {type(self)}')
|
raise NotImplementedError(f'items() method not implemented for {type(self)}')
|
||||||
|
|
||||||
def getUniqueParents(self):
|
def getUniqueParents(self) -> QuerySet:
|
||||||
"""Return a flat set of all parent items that exist above this node.
|
"""Return a flat set of all parent items that exist above this node."""
|
||||||
|
|
||||||
If any parents are repeated (which would be very bad!), the process is halted
|
|
||||||
"""
|
|
||||||
return self.get_ancestors()
|
return self.get_ancestors()
|
||||||
|
|
||||||
def getUniqueChildren(self, include_self=True):
|
def getUniqueChildren(self, include_self=True) -> QuerySet:
|
||||||
"""Return a flat set of all child items that exist under this node.
|
"""Return a flat set of all child items that exist under this node."""
|
||||||
|
|
||||||
If any child items are repeated, the repetitions are omitted.
|
|
||||||
"""
|
|
||||||
return self.get_descendants(include_self=include_self)
|
return self.get_descendants(include_self=include_self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_children(self):
|
def has_children(self) -> bool:
|
||||||
"""True if there are any children under this item."""
|
"""True if there are any children under this item."""
|
||||||
return self.getUniqueChildren(include_self=False).count() > 0
|
return self.getUniqueChildren(include_self=False).count() > 0
|
||||||
|
|
||||||
def getAcceptableParents(self):
|
def getAcceptableParents(self) -> list:
|
||||||
"""Returns a list of acceptable parent items within this model Acceptable parents are ones which are not underneath this item.
|
"""Returns a list of acceptable parent items within this model Acceptable parents are ones which are not underneath this item.
|
||||||
|
|
||||||
Setting the parent of an item to its own child results in recursion.
|
Setting the parent of an item to its own child results in recursion.
|
||||||
@ -860,7 +855,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
|||||||
return acceptable
|
return acceptable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parentpath(self):
|
def parentpath(self) -> list:
|
||||||
"""Get the parent path of this category.
|
"""Get the parent path of this category.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -869,7 +864,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
|||||||
return list(self.get_ancestors())
|
return list(self.get_ancestors())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self) -> list:
|
||||||
"""Get the complete part of this category.
|
"""Get the complete part of this category.
|
||||||
|
|
||||||
e.g. ["Top", "Second", "Third", "This"]
|
e.g. ["Top", "Second", "Third", "This"]
|
||||||
@ -879,7 +874,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
|||||||
"""
|
"""
|
||||||
return [*self.parentpath, self]
|
return [*self.parentpath, self]
|
||||||
|
|
||||||
def get_path(self):
|
def get_path(self) -> list:
|
||||||
"""Return a list of element in the item tree.
|
"""Return a list of element in the item tree.
|
||||||
|
|
||||||
Contains the full path to this item, with each entry containing the following data:
|
Contains the full path to this item, with each entry containing the following data:
|
||||||
|
@ -1152,24 +1152,24 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
|||||||
class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
||||||
"""Serializes a BuildItem object, which is an allocation of a stock item against a build order."""
|
"""Serializes a BuildItem object, which is an allocation of a stock item against a build order."""
|
||||||
|
|
||||||
# These fields are only used for data export
|
export_child_fields = [
|
||||||
export_only_fields = [
|
'build_detail.reference',
|
||||||
'bom_part_id',
|
'location_detail.name',
|
||||||
'bom_part_name',
|
'part_detail.name',
|
||||||
'build_reference',
|
'part_detail.description',
|
||||||
'sku',
|
'part_detail.IPN',
|
||||||
'mpn',
|
'stock_item_detail.batch',
|
||||||
'location_name',
|
'stock_item_detail.packaging',
|
||||||
'part_id',
|
'stock_item_detail.part',
|
||||||
'part_name',
|
'stock_item_detail.quantity',
|
||||||
'part_ipn',
|
'stock_item_detail.serial',
|
||||||
'part_description',
|
'supplier_part_detail.SKU',
|
||||||
'available_quantity',
|
'supplier_part_detail.MPN',
|
||||||
'item_batch_code',
|
|
||||||
'item_serial',
|
|
||||||
'item_packaging',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# These fields are only used for data export
|
||||||
|
export_only_fields = ['bom_part_id', 'bom_part_name']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Serializer metaclass."""
|
"""Serializer metaclass."""
|
||||||
|
|
||||||
@ -1192,18 +1192,6 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'bom_reference',
|
'bom_reference',
|
||||||
'bom_part_id',
|
'bom_part_id',
|
||||||
'bom_part_name',
|
'bom_part_name',
|
||||||
'build_reference',
|
|
||||||
'location_name',
|
|
||||||
'mpn',
|
|
||||||
'sku',
|
|
||||||
'part_id',
|
|
||||||
'part_name',
|
|
||||||
'part_ipn',
|
|
||||||
'part_description',
|
|
||||||
'available_quantity',
|
|
||||||
'item_batch_code',
|
|
||||||
'item_serial_number',
|
|
||||||
'item_packaging',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -1211,7 +1199,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
location_detail = kwargs.pop('location_detail', True)
|
location_detail = kwargs.pop('location_detail', True)
|
||||||
stock_detail = kwargs.pop('stock_detail', True)
|
stock_detail = kwargs.pop('stock_detail', True)
|
||||||
build_detail = kwargs.pop('build_detail', False)
|
build_detail = kwargs.pop('build_detail', True)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -1228,44 +1216,9 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
self.fields.pop('build_detail', None)
|
self.fields.pop('build_detail', None)
|
||||||
|
|
||||||
# Export-only fields
|
# Export-only fields
|
||||||
sku = serializers.CharField(
|
|
||||||
source='stock_item.supplier_part.SKU',
|
|
||||||
label=_('Supplier Part Number'),
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
mpn = serializers.CharField(
|
|
||||||
source='stock_item.supplier_part.manufacturer_part.MPN',
|
|
||||||
label=_('Manufacturer Part Number'),
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
location_name = serializers.CharField(
|
|
||||||
source='stock_item.location.name', label=_('Location Name'), read_only=True
|
|
||||||
)
|
|
||||||
build_reference = serializers.CharField(
|
|
||||||
source='build.reference', label=_('Build Reference'), read_only=True
|
|
||||||
)
|
|
||||||
bom_reference = serializers.CharField(
|
bom_reference = serializers.CharField(
|
||||||
source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True
|
source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True
|
||||||
)
|
)
|
||||||
item_packaging = serializers.CharField(
|
|
||||||
source='stock_item.packaging', label=_('Packaging'), read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Part detail fields
|
|
||||||
part_id = serializers.PrimaryKeyRelatedField(
|
|
||||||
source='stock_item.part', label=_('Part ID'), many=False, read_only=True
|
|
||||||
)
|
|
||||||
part_name = serializers.CharField(
|
|
||||||
source='stock_item.part.name', label=_('Part Name'), read_only=True
|
|
||||||
)
|
|
||||||
part_ipn = serializers.CharField(
|
|
||||||
source='stock_item.part.IPN', label=_('Part IPN'), read_only=True
|
|
||||||
)
|
|
||||||
part_description = serializers.CharField(
|
|
||||||
source='stock_item.part.description',
|
|
||||||
label=_('Part Description'),
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# BOM Item Part ID (it may be different to the allocated part)
|
# BOM Item Part ID (it may be different to the allocated part)
|
||||||
bom_part_id = serializers.PrimaryKeyRelatedField(
|
bom_part_id = serializers.PrimaryKeyRelatedField(
|
||||||
@ -1274,19 +1227,13 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
many=False,
|
many=False,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
bom_part_name = serializers.CharField(
|
bom_part_name = serializers.CharField(
|
||||||
source='build_line.bom_item.sub_part.name',
|
source='build_line.bom_item.sub_part.name',
|
||||||
label=_('BOM Part Name'),
|
label=_('BOM Part Name'),
|
||||||
read_only=True,
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
item_batch_code = serializers.CharField(
|
|
||||||
source='stock_item.batch', label=_('Batch Code'), read_only=True
|
|
||||||
)
|
|
||||||
item_serial_number = serializers.CharField(
|
|
||||||
source='stock_item.serial', label=_('Serial Number'), read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
build = serializers.PrimaryKeyRelatedField(
|
build = serializers.PrimaryKeyRelatedField(
|
||||||
source='build_line.build', many=False, read_only=True
|
source='build_line.build', many=False, read_only=True
|
||||||
@ -1294,26 +1241,38 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
|
|
||||||
# Extra (optional) detail fields
|
# Extra (optional) detail fields
|
||||||
part_detail = part_serializers.PartBriefSerializer(
|
part_detail = part_serializers.PartBriefSerializer(
|
||||||
source='stock_item.part', many=False, read_only=True, pricing=False
|
label=_('Part'),
|
||||||
|
source='stock_item.part',
|
||||||
|
many=False,
|
||||||
|
read_only=True,
|
||||||
|
pricing=False,
|
||||||
)
|
)
|
||||||
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
|
|
||||||
|
stock_item_detail = StockItemSerializerBrief(
|
||||||
|
source='stock_item', read_only=True, label=_('Stock Item')
|
||||||
|
)
|
||||||
|
|
||||||
location = serializers.PrimaryKeyRelatedField(
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
source='stock_item.location', many=False, read_only=True
|
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
location_detail = LocationBriefSerializer(
|
location_detail = LocationBriefSerializer(
|
||||||
source='stock_item.location', read_only=True
|
label=_('Location'), source='stock_item.location', read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
build_detail = BuildSerializer(
|
build_detail = BuildSerializer(
|
||||||
source='build_line.build', many=False, read_only=True
|
label=_('Build'), source='build_line.build', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_part_detail = company.serializers.SupplierPartSerializer(
|
supplier_part_detail = company.serializers.SupplierPartSerializer(
|
||||||
source='stock_item.supplier_part', many=False, read_only=True, brief=True
|
label=_('Supplier Part'),
|
||||||
|
source='stock_item.supplier_part',
|
||||||
|
many=False,
|
||||||
|
read_only=True,
|
||||||
|
brief=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
|
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
|
||||||
available_quantity = InvenTreeDecimalField(
|
|
||||||
source='stock_item.quantity', read_only=True, label=_('Available Quantity')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
||||||
@ -1321,7 +1280,16 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
|
|
||||||
export_exclude_fields = ['allocations']
|
export_exclude_fields = ['allocations']
|
||||||
|
|
||||||
export_only_fields = ['part_description', 'part_category_name']
|
export_child_fields = [
|
||||||
|
'build_detail.reference',
|
||||||
|
'part_detail.name',
|
||||||
|
'part_detail.description',
|
||||||
|
'part_detail.IPN',
|
||||||
|
'part_detail.category',
|
||||||
|
'bom_item_detail.reference',
|
||||||
|
]
|
||||||
|
|
||||||
|
export_only_fields = ['part_category_name']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Serializer metaclass."""
|
"""Serializer metaclass."""
|
||||||
@ -1332,6 +1300,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'build',
|
'build',
|
||||||
'bom_item',
|
'bom_item',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'part',
|
||||||
# Build detail fields
|
# Build detail fields
|
||||||
'build_reference',
|
'build_reference',
|
||||||
# BOM item detail fields
|
# BOM item detail fields
|
||||||
@ -1342,11 +1311,6 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'trackable',
|
'trackable',
|
||||||
'inherited',
|
'inherited',
|
||||||
'allow_variants',
|
'allow_variants',
|
||||||
# Part detail fields
|
|
||||||
'part',
|
|
||||||
'part_name',
|
|
||||||
'part_IPN',
|
|
||||||
'part_category_id',
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
'allocated',
|
'allocated',
|
||||||
'in_production',
|
'in_production',
|
||||||
@ -1358,7 +1322,6 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
# Related fields
|
# Related fields
|
||||||
'allocations',
|
'allocations',
|
||||||
# Extra fields only for data export
|
# Extra fields only for data export
|
||||||
'part_description',
|
|
||||||
'part_category_name',
|
'part_category_name',
|
||||||
# Extra detail (related field) serializers
|
# Extra detail (related field) serializers
|
||||||
'bom_item_detail',
|
'bom_item_detail',
|
||||||
@ -1371,7 +1334,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Determine which extra details fields should be included."""
|
"""Determine which extra details fields should be included."""
|
||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
build_detail = kwargs.pop('build_detail', False)
|
build_detail = kwargs.pop('build_detail', True)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -1390,21 +1353,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
part = serializers.PrimaryKeyRelatedField(
|
part = serializers.PrimaryKeyRelatedField(
|
||||||
source='bom_item.sub_part', label=_('Part'), many=False, read_only=True
|
source='bom_item.sub_part', label=_('Part'), many=False, read_only=True
|
||||||
)
|
)
|
||||||
part_name = serializers.CharField(
|
|
||||||
source='bom_item.sub_part.name', label=_('Part Name'), read_only=True
|
|
||||||
)
|
|
||||||
part_IPN = serializers.CharField( # noqa: N815
|
|
||||||
source='bom_item.sub_part.IPN', label=_('Part IPN'), read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
part_description = serializers.CharField(
|
|
||||||
source='bom_item.sub_part.description',
|
|
||||||
label=_('Part Description'),
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
part_category_id = serializers.PrimaryKeyRelatedField(
|
|
||||||
source='bom_item.sub_part.category', label=_('Part Category ID'), read_only=True
|
|
||||||
)
|
|
||||||
part_category_name = serializers.CharField(
|
part_category_name = serializers.CharField(
|
||||||
source='bom_item.sub_part.category.name',
|
source='bom_item.sub_part.category.name',
|
||||||
label=_('Part Category Name'),
|
label=_('Part Category Name'),
|
||||||
@ -1442,6 +1391,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
|
|
||||||
# Foreign key fields
|
# Foreign key fields
|
||||||
bom_item_detail = part_serializers.BomItemSerializer(
|
bom_item_detail = part_serializers.BomItemSerializer(
|
||||||
|
label=_('BOM Item'),
|
||||||
source='bom_item',
|
source='bom_item',
|
||||||
many=False,
|
many=False,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
@ -1452,10 +1402,14 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
)
|
)
|
||||||
|
|
||||||
part_detail = part_serializers.PartBriefSerializer(
|
part_detail = part_serializers.PartBriefSerializer(
|
||||||
source='bom_item.sub_part', many=False, read_only=True, pricing=False
|
label=_('Part'),
|
||||||
|
source='bom_item.sub_part',
|
||||||
|
many=False,
|
||||||
|
read_only=True,
|
||||||
|
pricing=False,
|
||||||
)
|
)
|
||||||
build_detail = BuildSerializer(
|
build_detail = BuildSerializer(
|
||||||
source='build', part_detail=False, many=False, read_only=True
|
label=_('Build'), source='build', part_detail=False, many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotated (calculated) fields
|
# Annotated (calculated) fields
|
||||||
|
@ -315,6 +315,16 @@ class SupplierPartSerializer(
|
|||||||
):
|
):
|
||||||
"""Serializer for SupplierPart object."""
|
"""Serializer for SupplierPart object."""
|
||||||
|
|
||||||
|
export_exclude_fields = ['tags']
|
||||||
|
|
||||||
|
export_child_fields = [
|
||||||
|
'part_detail.name',
|
||||||
|
'part_detail.description',
|
||||||
|
'part_detail.IPN',
|
||||||
|
'supplier_detail.name',
|
||||||
|
'manufacturer_detail.name',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
@ -327,12 +337,10 @@ class SupplierPartSerializer(
|
|||||||
'on_order',
|
'on_order',
|
||||||
'link',
|
'link',
|
||||||
'active',
|
'active',
|
||||||
'manufacturer',
|
|
||||||
'manufacturer_detail',
|
'manufacturer_detail',
|
||||||
'manufacturer_part',
|
'manufacturer_part',
|
||||||
'manufacturer_part_detail',
|
'manufacturer_part_detail',
|
||||||
'MPN',
|
'MPN',
|
||||||
'name',
|
|
||||||
'note',
|
'note',
|
||||||
'pk',
|
'pk',
|
||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
@ -393,6 +401,7 @@ class SupplierPartSerializer(
|
|||||||
if brief:
|
if brief:
|
||||||
self.fields.pop('tags')
|
self.fields.pop('tags')
|
||||||
self.fields.pop('available')
|
self.fields.pop('available')
|
||||||
|
self.fields.pop('on_order')
|
||||||
self.fields.pop('availability_updated')
|
self.fields.pop('availability_updated')
|
||||||
|
|
||||||
# Annotated field showing total in-stock quantity
|
# Annotated field showing total in-stock quantity
|
||||||
@ -405,32 +414,36 @@ class SupplierPartSerializer(
|
|||||||
pack_quantity_native = serializers.FloatField(read_only=True)
|
pack_quantity_native = serializers.FloatField(read_only=True)
|
||||||
|
|
||||||
part_detail = part_serializers.PartBriefSerializer(
|
part_detail = part_serializers.PartBriefSerializer(
|
||||||
source='part', many=False, read_only=True
|
label=_('Part'), source='part', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(
|
supplier_detail = CompanyBriefSerializer(
|
||||||
source='supplier', many=False, read_only=True
|
label=_('Supplier'), source='supplier', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer_detail = CompanyBriefSerializer(
|
manufacturer_detail = CompanyBriefSerializer(
|
||||||
source='manufacturer_part.manufacturer', many=False, read_only=True
|
label=_('Manufacturer'),
|
||||||
|
source='manufacturer_part.manufacturer',
|
||||||
|
many=False,
|
||||||
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
pretty_name = serializers.CharField(read_only=True)
|
pretty_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
supplier = serializers.PrimaryKeyRelatedField(
|
supplier = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=Company.objects.filter(is_supplier=True)
|
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
MPN = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
manufacturer_part_detail = ManufacturerPartSerializer(
|
manufacturer_part_detail = ManufacturerPartSerializer(
|
||||||
source='manufacturer_part', part_detail=False, read_only=True
|
label=_('Manufacturer Part'),
|
||||||
|
source='manufacturer_part',
|
||||||
|
part_detail=False,
|
||||||
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
name = serializers.CharField(read_only=True)
|
MPN = serializers.CharField(
|
||||||
|
source='manufacturer_part.MPN', read_only=True, label=_('MPN')
|
||||||
|
)
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import tablib
|
|||||||
from rest_framework import fields, serializers
|
from rest_framework import fields, serializers
|
||||||
from taggit.serializers import TagListSerializerField
|
from taggit.serializers import TagListSerializerField
|
||||||
|
|
||||||
import importer.operations
|
|
||||||
from InvenTree.helpers import DownloadFile, GetExportFormats, current_date
|
from InvenTree.helpers import DownloadFile, GetExportFormats, current_date
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +82,7 @@ class DataImportSerializerMixin:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip tags fields
|
# Skip tags fields
|
||||||
# TODO: Implement tag field support
|
# TODO: Implement tag field import support
|
||||||
if issubclass(field.__class__, TagListSerializerField):
|
if issubclass(field.__class__, TagListSerializerField):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -93,10 +92,17 @@ class DataImportSerializerMixin:
|
|||||||
|
|
||||||
|
|
||||||
class DataExportSerializerMixin:
|
class DataExportSerializerMixin:
|
||||||
"""Mixin class for adding data export functionality to a DRF serializer."""
|
"""Mixin class for adding data export functionality to a DRF serializer.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
export_only_fields: List of field names which are only used during data export
|
||||||
|
export_exclude_fields: List of field names which are excluded during data export
|
||||||
|
export_child_fields: List of child fields which are exported (using dot notation)
|
||||||
|
"""
|
||||||
|
|
||||||
export_only_fields = []
|
export_only_fields = []
|
||||||
export_exclude_fields = []
|
export_exclude_fields = []
|
||||||
|
export_child_fields = []
|
||||||
|
|
||||||
def get_export_only_fields(self, **kwargs) -> list:
|
def get_export_only_fields(self, **kwargs) -> list:
|
||||||
"""Return the list of field names which are only used during data export."""
|
"""Return the list of field names which are only used during data export."""
|
||||||
@ -142,20 +148,41 @@ class DataExportSerializerMixin:
|
|||||||
|
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
# Skip write-only fields
|
# Skip write-only fields
|
||||||
if getattr(field, 'write_only', False):
|
if getattr(field, 'write_only', False) or name in write_only_fields:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if name in write_only_fields:
|
# Skip tags fields
|
||||||
|
# TODO: Implement tag field export support
|
||||||
|
if issubclass(field.__class__, TagListSerializerField):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Top-level serializer fields can be exported with dot notation
|
||||||
# Skip fields which are themselves serializers
|
# Skip fields which are themselves serializers
|
||||||
if issubclass(field.__class__, serializers.Serializer):
|
if issubclass(field.__class__, serializers.Serializer):
|
||||||
|
fields.update(self.get_child_fields(name, field))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fields[name] = field
|
fields[name] = field
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
def get_child_fields(self, field_name: str, field) -> dict:
|
||||||
|
"""Return a dictionary of child fields for a given field.
|
||||||
|
|
||||||
|
Only child fields which match the 'export_child_fields' list will be returned.
|
||||||
|
"""
|
||||||
|
child_fields = {}
|
||||||
|
|
||||||
|
if sub_fields := getattr(field, 'fields', None):
|
||||||
|
for sub_name, sub_field in sub_fields.items():
|
||||||
|
name = f'{field_name}.{sub_name}'
|
||||||
|
|
||||||
|
if name in self.export_child_fields:
|
||||||
|
sub_field.parent_field = field
|
||||||
|
child_fields[name] = sub_field
|
||||||
|
|
||||||
|
return child_fields
|
||||||
|
|
||||||
def get_exported_filename(self, export_format) -> str:
|
def get_exported_filename(self, export_format) -> str:
|
||||||
"""Return the filename for the exported data file.
|
"""Return the filename for the exported data file.
|
||||||
|
|
||||||
@ -177,6 +204,33 @@ class DataExportSerializerMixin:
|
|||||||
"""Optional method to arrange the export headers."""
|
"""Optional method to arrange the export headers."""
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
def get_nested_value(self, row: dict, key: str) -> any:
|
||||||
|
"""Get a nested value from a dictionary.
|
||||||
|
|
||||||
|
This method allows for dot notation to access nested fields.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
row: The dictionary to extract the value from
|
||||||
|
key: The key to extract
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
any: The extracted value
|
||||||
|
"""
|
||||||
|
keys = key.split('.')
|
||||||
|
|
||||||
|
value = row
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
if not value:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = value.get(key, None)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
def process_row(self, row):
|
def process_row(self, row):
|
||||||
"""Optional method to process a row before exporting it."""
|
"""Optional method to process a row before exporting it."""
|
||||||
return row
|
return row
|
||||||
@ -203,13 +257,18 @@ class DataExportSerializerMixin:
|
|||||||
for field_name, field in fields.items():
|
for field_name, field in fields.items():
|
||||||
field = fields[field_name]
|
field = fields[field_name]
|
||||||
|
|
||||||
headers.append(importer.operations.get_field_label(field) or field_name)
|
label = getattr(field, 'label', field_name)
|
||||||
|
|
||||||
|
if parent := getattr(field, 'parent_field', None):
|
||||||
|
label = f'{parent.label}.{label}'
|
||||||
|
|
||||||
|
headers.append(label)
|
||||||
|
|
||||||
dataset = tablib.Dataset(headers=headers)
|
dataset = tablib.Dataset(headers=headers)
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
row = self.process_row(row)
|
row = self.process_row(row)
|
||||||
dataset.append([row.get(field, None) for field in field_names])
|
dataset.append([self.get_nested_value(row, f) for f in field_names])
|
||||||
|
|
||||||
return dataset.export(file_format)
|
return dataset.export(file_format)
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ def annotate_on_order_quantity(reference: str = ''):
|
|||||||
|
|
||||||
Note that in addition to the 'quantity' on order, we must also take into account 'pack_quantity'.
|
Note that in addition to the 'quantity' on order, we must also take into account 'pack_quantity'.
|
||||||
"""
|
"""
|
||||||
# Filter only 'active' purhase orders
|
# Filter only 'active' purchase orders
|
||||||
# Filter only line with outstanding quantity
|
# Filter only line with outstanding quantity
|
||||||
order_filter = Q(
|
order_filter = Q(
|
||||||
order__status__in=PurchaseOrderStatusGroups.OPEN, quantity__gt=F('received')
|
order__status__in=PurchaseOrderStatusGroups.OPEN, quantity__gt=F('received')
|
||||||
|
@ -1556,7 +1556,11 @@ class BomItemSerializer(
|
|||||||
|
|
||||||
import_exclude_fields = ['validated', 'substitutes']
|
import_exclude_fields = ['validated', 'substitutes']
|
||||||
|
|
||||||
export_only_fields = ['sub_part_name', 'sub_part_ipn', 'sub_part_description']
|
export_child_fields = [
|
||||||
|
'sub_part_detail.name',
|
||||||
|
'sub_part_detail.IPN',
|
||||||
|
'sub_part_detail.description',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass defining serializer fields."""
|
"""Metaclass defining serializer fields."""
|
||||||
@ -1565,10 +1569,6 @@ class BomItemSerializer(
|
|||||||
fields = [
|
fields = [
|
||||||
'part',
|
'part',
|
||||||
'sub_part',
|
'sub_part',
|
||||||
# Extra fields only for export
|
|
||||||
'sub_part_name',
|
|
||||||
'sub_part_ipn',
|
|
||||||
'sub_part_description',
|
|
||||||
'reference',
|
'reference',
|
||||||
'quantity',
|
'quantity',
|
||||||
'overage',
|
'overage',
|
||||||
@ -1646,17 +1646,8 @@ class BomItemSerializer(
|
|||||||
|
|
||||||
substitutes = BomItemSubstituteSerializer(many=True, read_only=True)
|
substitutes = BomItemSubstituteSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(
|
||||||
|
source='part', label=_('Assembly'), many=False, read_only=True
|
||||||
# Extra fields only for export
|
|
||||||
sub_part_name = serializers.CharField(
|
|
||||||
source='sub_part.name', read_only=True, label=_('Component Name')
|
|
||||||
)
|
|
||||||
sub_part_ipn = serializers.CharField(
|
|
||||||
source='sub_part.IPN', read_only=True, label=_('Component IPN')
|
|
||||||
)
|
|
||||||
sub_part_description = serializers.CharField(
|
|
||||||
source='sub_part.description', read_only=True, label=_('Component Description')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sub_part = serializers.PrimaryKeyRelatedField(
|
sub_part = serializers.PrimaryKeyRelatedField(
|
||||||
@ -1665,7 +1656,9 @@ class BomItemSerializer(
|
|||||||
help_text=_('Select the component part'),
|
help_text=_('Select the component part'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
|
sub_part_detail = PartBriefSerializer(
|
||||||
|
source='sub_part', label=_('Component'), many=False, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
on_order = serializers.FloatField(label=_('On Order'), read_only=True)
|
on_order = serializers.FloatField(label=_('On Order'), read_only=True)
|
||||||
|
|
||||||
|
@ -347,16 +347,23 @@ class StockLocationFilter(rest_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class StockLocationList(DataExportViewMixin, ListCreateAPI):
|
class StockLocationMixin:
|
||||||
"""API endpoint for list view of StockLocation objects.
|
"""Mixin class for StockLocation API endpoints."""
|
||||||
|
|
||||||
- GET: Return list of StockLocation objects
|
queryset = StockLocation.objects.all()
|
||||||
- POST: Create a new StockLocation
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = StockLocation.objects.all().prefetch_related('tags')
|
|
||||||
serializer_class = StockSerializers.LocationSerializer
|
serializer_class = StockSerializers.LocationSerializer
|
||||||
filterset_class = StockLocationFilter
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
"""Set context before returning serializer."""
|
||||||
|
try:
|
||||||
|
params = self.request.query_params
|
||||||
|
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||||
|
except AttributeError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
"""Return annotated queryset for the StockLocationList endpoint."""
|
"""Return annotated queryset for the StockLocationList endpoint."""
|
||||||
@ -364,6 +371,15 @@ class StockLocationList(DataExportViewMixin, ListCreateAPI):
|
|||||||
queryset = StockSerializers.LocationSerializer.annotate_queryset(queryset)
|
queryset = StockSerializers.LocationSerializer.annotate_queryset(queryset)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class StockLocationList(DataExportViewMixin, StockLocationMixin, ListCreateAPI):
|
||||||
|
"""API endpoint for list view of StockLocation objects.
|
||||||
|
|
||||||
|
- GET: Return list of StockLocation objects
|
||||||
|
- POST: Create a new StockLocation
|
||||||
|
"""
|
||||||
|
|
||||||
|
filterset_class = StockLocationFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
search_fields = ['name', 'description', 'pathstring', 'tags__name', 'tags__slug']
|
search_fields = ['name', 'description', 'pathstring', 'tags__name', 'tags__slug']
|
||||||
@ -373,6 +389,25 @@ class StockLocationList(DataExportViewMixin, ListCreateAPI):
|
|||||||
ordering = ['tree_id', 'lft', 'name']
|
ordering = ['tree_id', 'lft', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class StockLocationDetail(StockLocationMixin, CustomRetrieveUpdateDestroyAPI):
|
||||||
|
"""API endpoint for detail view of StockLocation object."""
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""Delete a Stock location instance via the API."""
|
||||||
|
delete_stock_items = str(request.data.get('delete_stock_items', 0)) == '1'
|
||||||
|
delete_sub_locations = str(request.data.get('delete_sub_locations', 0)) == '1'
|
||||||
|
|
||||||
|
return super().destroy(
|
||||||
|
request,
|
||||||
|
*args,
|
||||||
|
**dict(
|
||||||
|
kwargs,
|
||||||
|
delete_sub_locations=delete_sub_locations,
|
||||||
|
delete_stock_items=delete_stock_items,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StockLocationTree(ListAPI):
|
class StockLocationTree(ListAPI):
|
||||||
"""API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree."""
|
"""API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree."""
|
||||||
|
|
||||||
@ -1471,52 +1506,6 @@ class StockTrackingList(DataExportViewMixin, ListAPI):
|
|||||||
search_fields = ['title', 'notes']
|
search_fields = ['title', 'notes']
|
||||||
|
|
||||||
|
|
||||||
class LocationDetail(CustomRetrieveUpdateDestroyAPI):
|
|
||||||
"""API endpoint for detail view of StockLocation object.
|
|
||||||
|
|
||||||
- GET: Return a single StockLocation object
|
|
||||||
- PATCH: Update a StockLocation object
|
|
||||||
- DELETE: Remove a StockLocation object
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = StockLocation.objects.all()
|
|
||||||
serializer_class = StockSerializers.LocationSerializer
|
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
|
||||||
"""Add extra context to serializer based on provided query parameters."""
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
|
||||||
"""Return annotated queryset for the StockLocationList endpoint."""
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
|
||||||
queryset = StockSerializers.LocationSerializer.annotate_queryset(queryset)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
|
||||||
"""Delete a Stock location instance via the API."""
|
|
||||||
delete_stock_items = str(request.data.get('delete_stock_items', 0)) == '1'
|
|
||||||
delete_sub_locations = str(request.data.get('delete_sub_locations', 0)) == '1'
|
|
||||||
|
|
||||||
return super().destroy(
|
|
||||||
request,
|
|
||||||
*args,
|
|
||||||
**dict(
|
|
||||||
kwargs,
|
|
||||||
delete_sub_locations=delete_sub_locations,
|
|
||||||
delete_stock_items=delete_stock_items,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
stock_api_urls = [
|
stock_api_urls = [
|
||||||
path(
|
path(
|
||||||
'location/',
|
'location/',
|
||||||
@ -1532,7 +1521,7 @@ stock_api_urls = [
|
|||||||
{'model': StockLocation},
|
{'model': StockLocation},
|
||||||
name='api-location-metadata',
|
name='api-location-metadata',
|
||||||
),
|
),
|
||||||
path('', LocationDetail.as_view(), name='api-location-detail'),
|
path('', StockLocationDetail.as_view(), name='api-location-detail'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
path('', StockLocationList.as_view(), name='api-location-list'),
|
path('', StockLocationList.as_view(), name='api-location-list'),
|
||||||
|
@ -342,9 +342,20 @@ class StockItemSerializer(
|
|||||||
|
|
||||||
export_exclude_fields = ['tags', 'tracking_items']
|
export_exclude_fields = ['tags', 'tracking_items']
|
||||||
|
|
||||||
export_only_fields = ['part_pricing_min', 'part_pricing_max']
|
export_child_fields = [
|
||||||
|
'part_detail.name',
|
||||||
|
'part_detail.description',
|
||||||
|
'part_detail.IPN',
|
||||||
|
'part_detail.revision',
|
||||||
|
'part_detail.pricing_min',
|
||||||
|
'part_detail.pricing_max',
|
||||||
|
'location_detail.name',
|
||||||
|
'location_detail.pathstring',
|
||||||
|
'supplier_part_detail.SKU',
|
||||||
|
'supplier_part_detail.MPN',
|
||||||
|
]
|
||||||
|
|
||||||
import_exclude_fields = ['use_pack_size']
|
import_exclude_fields = ['use_pack_size', 'location_path']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
@ -357,8 +368,6 @@ class StockItemSerializer(
|
|||||||
'serial',
|
'serial',
|
||||||
'batch',
|
'batch',
|
||||||
'location',
|
'location',
|
||||||
'location_name',
|
|
||||||
'location_path',
|
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
'build',
|
'build',
|
||||||
'consumed_by',
|
'consumed_by',
|
||||||
@ -394,6 +403,7 @@ class StockItemSerializer(
|
|||||||
'expired',
|
'expired',
|
||||||
'installed_items',
|
'installed_items',
|
||||||
'child_items',
|
'child_items',
|
||||||
|
'location_path',
|
||||||
'stale',
|
'stale',
|
||||||
'tracking_items',
|
'tracking_items',
|
||||||
'tags',
|
'tags',
|
||||||
@ -401,9 +411,6 @@ class StockItemSerializer(
|
|||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'location_detail',
|
'location_detail',
|
||||||
# Export only fields
|
|
||||||
'part_pricing_min',
|
|
||||||
'part_pricing_max',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -426,11 +433,12 @@ class StockItemSerializer(
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Add detail fields."""
|
"""Add detail fields."""
|
||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
location_detail = kwargs.pop('location_detail', False)
|
location_detail = kwargs.pop('location_detail', True)
|
||||||
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
supplier_part_detail = kwargs.pop('supplier_part_detail', True)
|
||||||
tests = kwargs.pop('tests', False)
|
|
||||||
path_detail = kwargs.pop('path_detail', False)
|
path_detail = kwargs.pop('path_detail', False)
|
||||||
|
|
||||||
|
tests = kwargs.pop('tests', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if not part_detail:
|
if not part_detail:
|
||||||
@ -463,10 +471,6 @@ class StockItemSerializer(
|
|||||||
help_text=_('Parent stock item'),
|
help_text=_('Parent stock item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
location_name = serializers.CharField(
|
|
||||||
source='location.name', read_only=True, label=_('Location Name')
|
|
||||||
)
|
|
||||||
|
|
||||||
location_path = serializers.ListField(
|
location_path = serializers.ListField(
|
||||||
child=serializers.DictField(), source='location.get_path', read_only=True
|
child=serializers.DictField(), source='location.get_path', read_only=True
|
||||||
)
|
)
|
||||||
@ -504,7 +508,9 @@ class StockItemSerializer(
|
|||||||
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
|
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
'location',
|
'location',
|
||||||
|
'allocations',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
|
'sales_order_allocations',
|
||||||
'purchase_order',
|
'purchase_order',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'part',
|
'part',
|
||||||
@ -516,12 +522,17 @@ class StockItemSerializer(
|
|||||||
),
|
),
|
||||||
'parent',
|
'parent',
|
||||||
'part__category',
|
'part__category',
|
||||||
|
'part__supplier_parts',
|
||||||
|
'part__supplier_parts__purchase_order_line_items',
|
||||||
'part__pricing_data',
|
'part__pricing_data',
|
||||||
|
'part__tags',
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
'supplier_part__part',
|
'supplier_part__part',
|
||||||
'supplier_part__supplier',
|
'supplier_part__supplier',
|
||||||
'supplier_part__manufacturer_part',
|
'supplier_part__manufacturer_part',
|
||||||
'supplier_part__manufacturer_part__manufacturer',
|
'supplier_part__manufacturer_part__manufacturer',
|
||||||
|
'supplier_part__manufacturer_part__tags',
|
||||||
|
'supplier_part__purchase_order_line_items',
|
||||||
'supplier_part__tags',
|
'supplier_part__tags',
|
||||||
'test_results',
|
'test_results',
|
||||||
'customer',
|
'customer',
|
||||||
@ -593,7 +604,9 @@ class StockItemSerializer(
|
|||||||
|
|
||||||
# Optional detail fields, which can be appended via query parameters
|
# Optional detail fields, which can be appended via query parameters
|
||||||
supplier_part_detail = company_serializers.SupplierPartSerializer(
|
supplier_part_detail = company_serializers.SupplierPartSerializer(
|
||||||
|
label=_('Supplier Part'),
|
||||||
source='supplier_part',
|
source='supplier_part',
|
||||||
|
brief=True,
|
||||||
supplier_detail=False,
|
supplier_detail=False,
|
||||||
manufacturer_detail=False,
|
manufacturer_detail=False,
|
||||||
part_detail=False,
|
part_detail=False,
|
||||||
@ -602,11 +615,11 @@ class StockItemSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
part_detail = part_serializers.PartBriefSerializer(
|
part_detail = part_serializers.PartBriefSerializer(
|
||||||
source='part', many=False, read_only=True
|
label=_('Part'), source='part', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
location_detail = LocationBriefSerializer(
|
location_detail = LocationBriefSerializer(
|
||||||
source='location', many=False, read_only=True
|
label=_('Location'), source='location', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
tests = StockItemTestResultSerializer(
|
tests = StockItemTestResultSerializer(
|
||||||
@ -646,24 +659,13 @@ class StockItemSerializer(
|
|||||||
purchase_order_reference = serializers.CharField(
|
purchase_order_reference = serializers.CharField(
|
||||||
source='purchase_order.reference', read_only=True
|
source='purchase_order.reference', read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
sales_order_reference = serializers.CharField(
|
sales_order_reference = serializers.CharField(
|
||||||
source='sales_order.reference', read_only=True
|
source='sales_order.reference', read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
part_pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(
|
|
||||||
source='part.pricing_data.overall_min',
|
|
||||||
read_only=True,
|
|
||||||
label=_('Minimum Pricing'),
|
|
||||||
)
|
|
||||||
|
|
||||||
part_pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
|
||||||
source='part.pricing_data.overall_max',
|
|
||||||
read_only=True,
|
|
||||||
label=_('Maximum Pricing'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SerializeStockItemSerializer(serializers.Serializer):
|
class SerializeStockItemSerializer(serializers.Serializer):
|
||||||
"""A DRF serializer for "serializing" a StockItem.
|
"""A DRF serializer for "serializing" a StockItem.
|
||||||
@ -1185,6 +1187,9 @@ class LocationSerializer(
|
|||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Annotate extra information to the queryset."""
|
"""Annotate extra information to the queryset."""
|
||||||
# Annotate the number of stock items which exist in this category (including subcategories)
|
# Annotate the number of stock items which exist in this category (including subcategories)
|
||||||
|
|
||||||
|
queryset = queryset.prefetch_related('tags')
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
items=stock.filters.annotate_location_items(),
|
items=stock.filters.annotate_location_items(),
|
||||||
sublocations=stock.filters.annotate_sub_locations(),
|
sublocations=stock.filters.annotate_sub_locations(),
|
||||||
|
@ -871,10 +871,15 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
'Part',
|
'Part',
|
||||||
'Customer',
|
'Customer',
|
||||||
'Stock Location',
|
'Stock Location',
|
||||||
'Location Name',
|
|
||||||
'Parent Item',
|
'Parent Item',
|
||||||
'Quantity',
|
'Quantity',
|
||||||
'Status',
|
'Status',
|
||||||
|
'Part.Name',
|
||||||
|
'Part.Description',
|
||||||
|
'Location.Name',
|
||||||
|
'Location.Path',
|
||||||
|
'Supplier Part.SKU',
|
||||||
|
'Supplier Part.MPN',
|
||||||
]
|
]
|
||||||
|
|
||||||
for h in headers:
|
for h in headers:
|
||||||
@ -886,12 +891,35 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertNotIn(h, dataset.headers)
|
self.assertNotIn(h, dataset.headers)
|
||||||
|
|
||||||
# Now, add a filter to the results
|
# Now, add a filter to the results
|
||||||
dataset = self.export_data({'location': 1})
|
dataset = self.export_data({'location': 1, 'cascade': True})
|
||||||
|
|
||||||
self.assertEqual(len(dataset), 9)
|
self.assertEqual(len(dataset), 9)
|
||||||
|
|
||||||
dataset = self.export_data({'part': 25})
|
# Read out the data
|
||||||
|
idx_id = dataset.headers.index('ID')
|
||||||
|
idx_loc = dataset.headers.index('Stock Location')
|
||||||
|
idx_loc_name = dataset.headers.index('Location.Name')
|
||||||
|
idx_part_name = dataset.headers.index('Part.Name')
|
||||||
|
|
||||||
|
for row in dataset:
|
||||||
|
item_id = int(row[idx_id])
|
||||||
|
item = StockItem.objects.get(pk=item_id)
|
||||||
|
|
||||||
|
loc_id = int(row[idx_loc])
|
||||||
|
|
||||||
|
# Location should match ID
|
||||||
|
self.assertEqual(int(loc_id), item.location.pk)
|
||||||
|
|
||||||
|
# Location name should match
|
||||||
|
loc_name = row[idx_loc_name]
|
||||||
|
self.assertEqual(loc_name, item.location.name)
|
||||||
|
|
||||||
|
# Part name should match
|
||||||
|
part_name = row[idx_part_name]
|
||||||
|
self.assertEqual(part_name, item.part.name)
|
||||||
|
|
||||||
|
# Export stock items with a specific part
|
||||||
|
dataset = self.export_data({'part': 25})
|
||||||
self.assertEqual(len(dataset), 17)
|
self.assertEqual(len(dataset), 17)
|
||||||
|
|
||||||
def test_filter_by_allocated(self):
|
def test_filter_by_allocated(self):
|
||||||
|
@ -64,7 +64,8 @@ export default function SupplierPartDetail() {
|
|||||||
hasPrimaryKey: true,
|
hasPrimaryKey: true,
|
||||||
params: {
|
params: {
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
supplier_detail: true
|
supplier_detail: true,
|
||||||
|
manufacturer_detail: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user