mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +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:
		@@ -1,13 +1,17 @@
 | 
			
		||||
"""InvenTree API version information."""
 | 
			
		||||
 | 
			
		||||
# 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."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    - 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.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import QuerySet
 | 
			
		||||
from django.db.models.signals import post_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
@@ -820,26 +821,20 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError(f'items() method not implemented for {type(self)}')
 | 
			
		||||
 | 
			
		||||
    def getUniqueParents(self):
 | 
			
		||||
        """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
 | 
			
		||||
        """
 | 
			
		||||
    def getUniqueParents(self) -> QuerySet:
 | 
			
		||||
        """Return a flat set of all parent items that exist above this node."""
 | 
			
		||||
        return self.get_ancestors()
 | 
			
		||||
 | 
			
		||||
    def getUniqueChildren(self, include_self=True):
 | 
			
		||||
        """Return a flat set of all child items that exist under this node.
 | 
			
		||||
 | 
			
		||||
        If any child items are repeated, the repetitions are omitted.
 | 
			
		||||
        """
 | 
			
		||||
    def getUniqueChildren(self, include_self=True) -> QuerySet:
 | 
			
		||||
        """Return a flat set of all child items that exist under this node."""
 | 
			
		||||
        return self.get_descendants(include_self=include_self)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_children(self):
 | 
			
		||||
    def has_children(self) -> bool:
 | 
			
		||||
        """True if there are any children under this item."""
 | 
			
		||||
        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.
 | 
			
		||||
 | 
			
		||||
        Setting the parent of an item to its own child results in recursion.
 | 
			
		||||
@@ -860,7 +855,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
 | 
			
		||||
        return acceptable
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def parentpath(self):
 | 
			
		||||
    def parentpath(self) -> list:
 | 
			
		||||
        """Get the parent path of this category.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
@@ -869,7 +864,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
 | 
			
		||||
        return list(self.get_ancestors())
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def path(self):
 | 
			
		||||
    def path(self) -> list:
 | 
			
		||||
        """Get the complete part of this category.
 | 
			
		||||
 | 
			
		||||
        e.g. ["Top", "Second", "Third", "This"]
 | 
			
		||||
@@ -879,7 +874,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
 | 
			
		||||
        """
 | 
			
		||||
        return [*self.parentpath, self]
 | 
			
		||||
 | 
			
		||||
    def get_path(self):
 | 
			
		||||
    def get_path(self) -> list:
 | 
			
		||||
        """Return a list of element in the item tree.
 | 
			
		||||
 | 
			
		||||
        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):
 | 
			
		||||
    """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_only_fields = [
 | 
			
		||||
        'bom_part_id',
 | 
			
		||||
        'bom_part_name',
 | 
			
		||||
        'build_reference',
 | 
			
		||||
        'sku',
 | 
			
		||||
        'mpn',
 | 
			
		||||
        'location_name',
 | 
			
		||||
        'part_id',
 | 
			
		||||
        'part_name',
 | 
			
		||||
        'part_ipn',
 | 
			
		||||
        'part_description',
 | 
			
		||||
        'available_quantity',
 | 
			
		||||
        'item_batch_code',
 | 
			
		||||
        'item_serial',
 | 
			
		||||
        'item_packaging',
 | 
			
		||||
    export_child_fields = [
 | 
			
		||||
        'build_detail.reference',
 | 
			
		||||
        'location_detail.name',
 | 
			
		||||
        'part_detail.name',
 | 
			
		||||
        'part_detail.description',
 | 
			
		||||
        'part_detail.IPN',
 | 
			
		||||
        'stock_item_detail.batch',
 | 
			
		||||
        'stock_item_detail.packaging',
 | 
			
		||||
        'stock_item_detail.part',
 | 
			
		||||
        'stock_item_detail.quantity',
 | 
			
		||||
        'stock_item_detail.serial',
 | 
			
		||||
        'supplier_part_detail.SKU',
 | 
			
		||||
        'supplier_part_detail.MPN',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # These fields are only used for data export
 | 
			
		||||
    export_only_fields = ['bom_part_id', 'bom_part_name']
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Serializer metaclass."""
 | 
			
		||||
 | 
			
		||||
@@ -1192,18 +1192,6 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
            'bom_reference',
 | 
			
		||||
            'bom_part_id',
 | 
			
		||||
            '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):
 | 
			
		||||
@@ -1211,7 +1199,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
        part_detail = kwargs.pop('part_detail', True)
 | 
			
		||||
        location_detail = kwargs.pop('location_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)
 | 
			
		||||
 | 
			
		||||
@@ -1228,44 +1216,9 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
            self.fields.pop('build_detail', None)
 | 
			
		||||
 | 
			
		||||
    # 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(
 | 
			
		||||
        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_part_id = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
@@ -1274,19 +1227,13 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
        many=False,
 | 
			
		||||
        read_only=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    bom_part_name = serializers.CharField(
 | 
			
		||||
        source='build_line.bom_item.sub_part.name',
 | 
			
		||||
        label=_('BOM Part Name'),
 | 
			
		||||
        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
 | 
			
		||||
    build = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        source='build_line.build', many=False, read_only=True
 | 
			
		||||
@@ -1294,26 +1241,38 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
 | 
			
		||||
    # Extra (optional) detail fields
 | 
			
		||||
    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(
 | 
			
		||||
        source='stock_item.location', many=False, read_only=True
 | 
			
		||||
        label=_('Location'), source='stock_item.location', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    location_detail = LocationBriefSerializer(
 | 
			
		||||
        source='stock_item.location', read_only=True
 | 
			
		||||
        label=_('Location'), source='stock_item.location', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
        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'))
 | 
			
		||||
    available_quantity = InvenTreeDecimalField(
 | 
			
		||||
        source='stock_item.quantity', read_only=True, label=_('Available Quantity')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
 | 
			
		||||
@@ -1321,7 +1280,16 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
        """Serializer metaclass."""
 | 
			
		||||
@@ -1332,6 +1300,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
            'build',
 | 
			
		||||
            'bom_item',
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'part',
 | 
			
		||||
            # Build detail fields
 | 
			
		||||
            'build_reference',
 | 
			
		||||
            # BOM item detail fields
 | 
			
		||||
@@ -1342,11 +1311,6 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
            'trackable',
 | 
			
		||||
            'inherited',
 | 
			
		||||
            'allow_variants',
 | 
			
		||||
            # Part detail fields
 | 
			
		||||
            'part',
 | 
			
		||||
            'part_name',
 | 
			
		||||
            'part_IPN',
 | 
			
		||||
            'part_category_id',
 | 
			
		||||
            # Annotated fields
 | 
			
		||||
            'allocated',
 | 
			
		||||
            'in_production',
 | 
			
		||||
@@ -1358,7 +1322,6 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
            # Related fields
 | 
			
		||||
            'allocations',
 | 
			
		||||
            # Extra fields only for data export
 | 
			
		||||
            'part_description',
 | 
			
		||||
            'part_category_name',
 | 
			
		||||
            # Extra detail (related field) serializers
 | 
			
		||||
            'bom_item_detail',
 | 
			
		||||
@@ -1371,7 +1334,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Determine which extra details fields should be included."""
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
@@ -1390,21 +1353,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
    part = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        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(
 | 
			
		||||
        source='bom_item.sub_part.category.name',
 | 
			
		||||
        label=_('Part Category Name'),
 | 
			
		||||
@@ -1442,6 +1391,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
 | 
			
		||||
    # Foreign key fields
 | 
			
		||||
    bom_item_detail = part_serializers.BomItemSerializer(
 | 
			
		||||
        label=_('BOM Item'),
 | 
			
		||||
        source='bom_item',
 | 
			
		||||
        many=False,
 | 
			
		||||
        read_only=True,
 | 
			
		||||
@@ -1452,10 +1402,14 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -315,6 +315,16 @@ class SupplierPartSerializer(
 | 
			
		||||
):
 | 
			
		||||
    """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:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
@@ -327,12 +337,10 @@ class SupplierPartSerializer(
 | 
			
		||||
            'on_order',
 | 
			
		||||
            'link',
 | 
			
		||||
            'active',
 | 
			
		||||
            'manufacturer',
 | 
			
		||||
            'manufacturer_detail',
 | 
			
		||||
            'manufacturer_part',
 | 
			
		||||
            'manufacturer_part_detail',
 | 
			
		||||
            'MPN',
 | 
			
		||||
            'name',
 | 
			
		||||
            'note',
 | 
			
		||||
            'pk',
 | 
			
		||||
            'barcode_hash',
 | 
			
		||||
@@ -393,6 +401,7 @@ class SupplierPartSerializer(
 | 
			
		||||
        if brief:
 | 
			
		||||
            self.fields.pop('tags')
 | 
			
		||||
            self.fields.pop('available')
 | 
			
		||||
            self.fields.pop('on_order')
 | 
			
		||||
            self.fields.pop('availability_updated')
 | 
			
		||||
 | 
			
		||||
    # Annotated field showing total in-stock quantity
 | 
			
		||||
@@ -405,32 +414,36 @@ class SupplierPartSerializer(
 | 
			
		||||
    pack_quantity_native = serializers.FloatField(read_only=True)
 | 
			
		||||
 | 
			
		||||
    part_detail = part_serializers.PartBriefSerializer(
 | 
			
		||||
        source='part', many=False, read_only=True
 | 
			
		||||
        label=_('Part'), source='part', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    supplier_detail = CompanyBriefSerializer(
 | 
			
		||||
        source='supplier', many=False, read_only=True
 | 
			
		||||
        label=_('Supplier'), source='supplier', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import tablib
 | 
			
		||||
from rest_framework import fields, serializers
 | 
			
		||||
from taggit.serializers import TagListSerializerField
 | 
			
		||||
 | 
			
		||||
import importer.operations
 | 
			
		||||
from InvenTree.helpers import DownloadFile, GetExportFormats, current_date
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +82,7 @@ class DataImportSerializerMixin:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # Skip tags fields
 | 
			
		||||
            # TODO: Implement tag field support
 | 
			
		||||
            # TODO: Implement tag field import support
 | 
			
		||||
            if issubclass(field.__class__, TagListSerializerField):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
@@ -93,10 +92,17 @@ class DataImportSerializerMixin:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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_exclude_fields = []
 | 
			
		||||
    export_child_fields = []
 | 
			
		||||
 | 
			
		||||
    def get_export_only_fields(self, **kwargs) -> list:
 | 
			
		||||
        """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():
 | 
			
		||||
            # Skip write-only fields
 | 
			
		||||
            if getattr(field, 'write_only', False):
 | 
			
		||||
            if getattr(field, 'write_only', False) or name in write_only_fields:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if name in write_only_fields:
 | 
			
		||||
            # Skip tags fields
 | 
			
		||||
            # TODO: Implement tag field export support
 | 
			
		||||
            if issubclass(field.__class__, TagListSerializerField):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # Top-level serializer fields can be exported with dot notation
 | 
			
		||||
            # Skip fields which are themselves serializers
 | 
			
		||||
            if issubclass(field.__class__, serializers.Serializer):
 | 
			
		||||
                fields.update(self.get_child_fields(name, field))
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            fields[name] = field
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
        """Return the filename for the exported data file.
 | 
			
		||||
 | 
			
		||||
@@ -177,6 +204,33 @@ class DataExportSerializerMixin:
 | 
			
		||||
        """Optional method to arrange the export 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):
 | 
			
		||||
        """Optional method to process a row before exporting it."""
 | 
			
		||||
        return row
 | 
			
		||||
@@ -203,13 +257,18 @@ class DataExportSerializerMixin:
 | 
			
		||||
        for field_name, field in fields.items():
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
        for row in data:
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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'.
 | 
			
		||||
    """
 | 
			
		||||
    # Filter only 'active' purhase orders
 | 
			
		||||
    # Filter only 'active' purchase orders
 | 
			
		||||
    # Filter only line with outstanding quantity
 | 
			
		||||
    order_filter = Q(
 | 
			
		||||
        order__status__in=PurchaseOrderStatusGroups.OPEN, quantity__gt=F('received')
 | 
			
		||||
 
 | 
			
		||||
@@ -1556,7 +1556,11 @@ class BomItemSerializer(
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
        """Metaclass defining serializer fields."""
 | 
			
		||||
@@ -1565,10 +1569,6 @@ class BomItemSerializer(
 | 
			
		||||
        fields = [
 | 
			
		||||
            'part',
 | 
			
		||||
            'sub_part',
 | 
			
		||||
            # Extra fields only for export
 | 
			
		||||
            'sub_part_name',
 | 
			
		||||
            'sub_part_ipn',
 | 
			
		||||
            'sub_part_description',
 | 
			
		||||
            'reference',
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'overage',
 | 
			
		||||
@@ -1646,17 +1646,8 @@ class BomItemSerializer(
 | 
			
		||||
 | 
			
		||||
    substitutes = BomItemSubstituteSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    part_detail = PartBriefSerializer(source='part', 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')
 | 
			
		||||
    part_detail = PartBriefSerializer(
 | 
			
		||||
        source='part', label=_('Assembly'), many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    sub_part = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
@@ -1665,7 +1656,9 @@ class BomItemSerializer(
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -347,16 +347,23 @@ class StockLocationFilter(rest_filters.FilterSet):
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockLocationList(DataExportViewMixin, ListCreateAPI):
 | 
			
		||||
    """API endpoint for list view of StockLocation objects.
 | 
			
		||||
class StockLocationMixin:
 | 
			
		||||
    """Mixin class for StockLocation API endpoints."""
 | 
			
		||||
 | 
			
		||||
    - GET: Return list of StockLocation objects
 | 
			
		||||
    - POST: Create a new StockLocation
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    queryset = StockLocation.objects.all().prefetch_related('tags')
 | 
			
		||||
    queryset = StockLocation.objects.all()
 | 
			
		||||
    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):
 | 
			
		||||
        """Return annotated queryset for the StockLocationList endpoint."""
 | 
			
		||||
@@ -364,6 +371,15 @@ class StockLocationList(DataExportViewMixin, ListCreateAPI):
 | 
			
		||||
        queryset = StockSerializers.LocationSerializer.annotate_queryset(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
 | 
			
		||||
 | 
			
		||||
    search_fields = ['name', 'description', 'pathstring', 'tags__name', 'tags__slug']
 | 
			
		||||
@@ -373,6 +389,25 @@ class StockLocationList(DataExportViewMixin, ListCreateAPI):
 | 
			
		||||
    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):
 | 
			
		||||
    """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']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 = [
 | 
			
		||||
    path(
 | 
			
		||||
        'location/',
 | 
			
		||||
@@ -1532,7 +1521,7 @@ stock_api_urls = [
 | 
			
		||||
                        {'model': StockLocation},
 | 
			
		||||
                        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'),
 | 
			
		||||
 
 | 
			
		||||
@@ -342,9 +342,20 @@ class StockItemSerializer(
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
@@ -357,8 +368,6 @@ class StockItemSerializer(
 | 
			
		||||
            'serial',
 | 
			
		||||
            'batch',
 | 
			
		||||
            'location',
 | 
			
		||||
            'location_name',
 | 
			
		||||
            'location_path',
 | 
			
		||||
            'belongs_to',
 | 
			
		||||
            'build',
 | 
			
		||||
            'consumed_by',
 | 
			
		||||
@@ -394,6 +403,7 @@ class StockItemSerializer(
 | 
			
		||||
            'expired',
 | 
			
		||||
            'installed_items',
 | 
			
		||||
            'child_items',
 | 
			
		||||
            'location_path',
 | 
			
		||||
            'stale',
 | 
			
		||||
            'tracking_items',
 | 
			
		||||
            'tags',
 | 
			
		||||
@@ -401,9 +411,6 @@ class StockItemSerializer(
 | 
			
		||||
            'supplier_part_detail',
 | 
			
		||||
            'part_detail',
 | 
			
		||||
            'location_detail',
 | 
			
		||||
            # Export only fields
 | 
			
		||||
            'part_pricing_min',
 | 
			
		||||
            'part_pricing_max',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
@@ -426,11 +433,12 @@ class StockItemSerializer(
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Add detail fields."""
 | 
			
		||||
        part_detail = kwargs.pop('part_detail', True)
 | 
			
		||||
        location_detail = kwargs.pop('location_detail', False)
 | 
			
		||||
        supplier_part_detail = kwargs.pop('supplier_part_detail', False)
 | 
			
		||||
        tests = kwargs.pop('tests', False)
 | 
			
		||||
        location_detail = kwargs.pop('location_detail', True)
 | 
			
		||||
        supplier_part_detail = kwargs.pop('supplier_part_detail', True)
 | 
			
		||||
        path_detail = kwargs.pop('path_detail', False)
 | 
			
		||||
 | 
			
		||||
        tests = kwargs.pop('tests', False)
 | 
			
		||||
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        if not part_detail:
 | 
			
		||||
@@ -463,10 +471,6 @@ class StockItemSerializer(
 | 
			
		||||
        help_text=_('Parent stock item'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    location_name = serializers.CharField(
 | 
			
		||||
        source='location.name', read_only=True, label=_('Location Name')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    location_path = serializers.ListField(
 | 
			
		||||
        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."""
 | 
			
		||||
        queryset = queryset.prefetch_related(
 | 
			
		||||
            'location',
 | 
			
		||||
            'allocations',
 | 
			
		||||
            'sales_order',
 | 
			
		||||
            'sales_order_allocations',
 | 
			
		||||
            'purchase_order',
 | 
			
		||||
            Prefetch(
 | 
			
		||||
                'part',
 | 
			
		||||
@@ -516,12 +522,17 @@ class StockItemSerializer(
 | 
			
		||||
            ),
 | 
			
		||||
            'parent',
 | 
			
		||||
            'part__category',
 | 
			
		||||
            'part__supplier_parts',
 | 
			
		||||
            'part__supplier_parts__purchase_order_line_items',
 | 
			
		||||
            'part__pricing_data',
 | 
			
		||||
            'part__tags',
 | 
			
		||||
            'supplier_part',
 | 
			
		||||
            'supplier_part__part',
 | 
			
		||||
            'supplier_part__supplier',
 | 
			
		||||
            'supplier_part__manufacturer_part',
 | 
			
		||||
            'supplier_part__manufacturer_part__manufacturer',
 | 
			
		||||
            'supplier_part__manufacturer_part__tags',
 | 
			
		||||
            'supplier_part__purchase_order_line_items',
 | 
			
		||||
            'supplier_part__tags',
 | 
			
		||||
            'test_results',
 | 
			
		||||
            'customer',
 | 
			
		||||
@@ -593,7 +604,9 @@ class StockItemSerializer(
 | 
			
		||||
 | 
			
		||||
    # Optional detail fields, which can be appended via query parameters
 | 
			
		||||
    supplier_part_detail = company_serializers.SupplierPartSerializer(
 | 
			
		||||
        label=_('Supplier Part'),
 | 
			
		||||
        source='supplier_part',
 | 
			
		||||
        brief=True,
 | 
			
		||||
        supplier_detail=False,
 | 
			
		||||
        manufacturer_detail=False,
 | 
			
		||||
        part_detail=False,
 | 
			
		||||
@@ -602,11 +615,11 @@ class StockItemSerializer(
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    part_detail = part_serializers.PartBriefSerializer(
 | 
			
		||||
        source='part', many=False, read_only=True
 | 
			
		||||
        label=_('Part'), source='part', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    location_detail = LocationBriefSerializer(
 | 
			
		||||
        source='location', many=False, read_only=True
 | 
			
		||||
        label=_('Location'), source='location', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    tests = StockItemTestResultSerializer(
 | 
			
		||||
@@ -646,24 +659,13 @@ class StockItemSerializer(
 | 
			
		||||
    purchase_order_reference = serializers.CharField(
 | 
			
		||||
        source='purchase_order.reference', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    sales_order_reference = serializers.CharField(
 | 
			
		||||
        source='sales_order.reference', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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):
 | 
			
		||||
    """A DRF serializer for "serializing" a StockItem.
 | 
			
		||||
@@ -1185,6 +1187,9 @@ class LocationSerializer(
 | 
			
		||||
    def annotate_queryset(queryset):
 | 
			
		||||
        """Annotate extra information to the queryset."""
 | 
			
		||||
        # Annotate the number of stock items which exist in this category (including subcategories)
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.prefetch_related('tags')
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            items=stock.filters.annotate_location_items(),
 | 
			
		||||
            sublocations=stock.filters.annotate_sub_locations(),
 | 
			
		||||
 
 | 
			
		||||
@@ -871,10 +871,15 @@ class StockItemListTest(StockAPITestCase):
 | 
			
		||||
            'Part',
 | 
			
		||||
            'Customer',
 | 
			
		||||
            'Stock Location',
 | 
			
		||||
            'Location Name',
 | 
			
		||||
            'Parent Item',
 | 
			
		||||
            'Quantity',
 | 
			
		||||
            'Status',
 | 
			
		||||
            'Part.Name',
 | 
			
		||||
            'Part.Description',
 | 
			
		||||
            'Location.Name',
 | 
			
		||||
            'Location.Path',
 | 
			
		||||
            'Supplier Part.SKU',
 | 
			
		||||
            'Supplier Part.MPN',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for h in headers:
 | 
			
		||||
@@ -886,12 +891,35 @@ class StockItemListTest(StockAPITestCase):
 | 
			
		||||
            self.assertNotIn(h, dataset.headers)
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
    def test_filter_by_allocated(self):
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,8 @@ export default function SupplierPartDetail() {
 | 
			
		||||
    hasPrimaryKey: true,
 | 
			
		||||
    params: {
 | 
			
		||||
      part_detail: true,
 | 
			
		||||
      supplier_detail: true
 | 
			
		||||
      supplier_detail: true,
 | 
			
		||||
      manufacturer_detail: true
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user