mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-12 10:12:58 +00:00
[WIP] Build line filter fix (#8343)
* Remove 'allocations' from BuildLineSerializer - Expensive to have a "many" serializer automatically used - Adjust existing tables accordingly - Fetch on demand * WIP: Add some unit tests * Adjust BuildLine queryset annotation - Multi-level annotation proves to be very expensive - Reduce complexity, save a bunch of time on queries - Remove 'total_allocated_stock' field - Adjust API query filter * Optimize query by deferring certain fields * Further query refinements * Bump API version
This commit is contained in:
parent
075b62757a
commit
6be6c4b42f
@ -1,13 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 271
|
INVENTREE_API_VERSION = 272
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v272 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
|
||||||
|
- Adjustments to BuildLine API serializers
|
||||||
|
|
||||||
v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
|
v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
|
||||||
- Fixes for SalesOrderLineItem endpoints
|
- Fixes for SalesOrderLineItem endpoints
|
||||||
|
|
||||||
|
@ -370,10 +370,10 @@ class BuildLineFilter(rest_filters.FilterSet):
|
|||||||
To determine this, we need to know:
|
To determine this, we need to know:
|
||||||
|
|
||||||
- The quantity required for each BuildLine
|
- The quantity required for each BuildLine
|
||||||
- The quantity available for each BuildLine
|
- The quantity available for each BuildLine (including variants and substitutes)
|
||||||
- The quantity allocated for each BuildLine
|
- The quantity allocated for each BuildLine
|
||||||
"""
|
"""
|
||||||
flt = Q(quantity__lte=F('total_available_stock') + F('allocated'))
|
flt = Q(quantity__lte=F('allocated') + F('available_stock') + F('available_substitute_stock') + F('available_variant_stock'))
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.filter(flt)
|
return queryset.filter(flt)
|
||||||
@ -399,10 +399,13 @@ class BuildLineEndpoint:
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override queryset to select-related and annotate"""
|
"""Override queryset to select-related and annotate"""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
source_build = self.get_source_build()
|
|
||||||
queryset = build.serializers.BuildLineSerializer.annotate_queryset(queryset, build=source_build)
|
|
||||||
|
|
||||||
return queryset
|
if not hasattr(self, 'source_build'):
|
||||||
|
self.source_build = self.get_source_build()
|
||||||
|
|
||||||
|
source_build = self.source_build
|
||||||
|
|
||||||
|
return build.serializers.BuildLineSerializer.annotate_queryset(queryset, build=source_build)
|
||||||
|
|
||||||
|
|
||||||
class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
|
class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
|
||||||
@ -446,15 +449,17 @@ class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI):
|
|||||||
def get_source_build(self) -> Build | None:
|
def get_source_build(self) -> Build | None:
|
||||||
"""Return the target build for the BuildLine queryset."""
|
"""Return the target build for the BuildLine queryset."""
|
||||||
|
|
||||||
|
source_build = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
build_id = self.request.query_params.get('build', None)
|
build_id = self.request.query_params.get('build', None)
|
||||||
if build_id:
|
if build_id:
|
||||||
build = Build.objects.get(pk=build_id)
|
source_build = Build.objects.filter(pk=build_id).first()
|
||||||
return build
|
|
||||||
except (Build.DoesNotExist, AttributeError, ValueError):
|
except (Build.DoesNotExist, AttributeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return None
|
return source_build
|
||||||
|
|
||||||
|
|
||||||
class BuildLineDetail(BuildLineEndpoint, RetrieveUpdateDestroyAPI):
|
class BuildLineDetail(BuildLineEndpoint, RetrieveUpdateDestroyAPI):
|
||||||
"""API endpoint for detail view of a BuildLine object."""
|
"""API endpoint for detail view of a BuildLine object."""
|
||||||
|
@ -1279,7 +1279,9 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'bom_item_detail',
|
'bom_item_detail',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'quantity',
|
'quantity',
|
||||||
'allocations',
|
|
||||||
|
# Build detail fields
|
||||||
|
'build_reference',
|
||||||
|
|
||||||
# BOM item detail fields
|
# BOM item detail fields
|
||||||
'reference',
|
'reference',
|
||||||
@ -1303,7 +1305,6 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'available_stock',
|
'available_stock',
|
||||||
'available_substitute_stock',
|
'available_substitute_stock',
|
||||||
'available_variant_stock',
|
'available_variant_stock',
|
||||||
'total_available_stock',
|
|
||||||
'external_stock',
|
'external_stock',
|
||||||
|
|
||||||
# Extra fields only for data export
|
# Extra fields only for data export
|
||||||
@ -1317,6 +1318,9 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'allocations',
|
'allocations',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Build info fields
|
||||||
|
build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True)
|
||||||
|
|
||||||
# Part info fields
|
# Part info fields
|
||||||
part = serializers.PrimaryKeyRelatedField(source='bom_item.sub_part', label=_('Part'), many=False, read_only=True)
|
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_name = serializers.CharField(source='bom_item.sub_part.name', label=_('Part Name'), read_only=True)
|
||||||
@ -1340,9 +1344,17 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
||||||
|
|
||||||
# Foreign key fields
|
# Foreign key fields
|
||||||
bom_item_detail = part_serializers.BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False)
|
bom_item_detail = part_serializers.BomItemSerializer(
|
||||||
|
source='bom_item',
|
||||||
|
many=False,
|
||||||
|
read_only=True,
|
||||||
|
pricing=False,
|
||||||
|
substitutes=False,
|
||||||
|
sub_part_detail=False,
|
||||||
|
part_detail=False
|
||||||
|
)
|
||||||
|
|
||||||
part_detail = part_serializers.PartBriefSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
|
part_detail = part_serializers.PartBriefSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
|
||||||
allocations = BuildItemSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
# Annotated (calculated) fields
|
# Annotated (calculated) fields
|
||||||
allocated = serializers.FloatField(
|
allocated = serializers.FloatField(
|
||||||
@ -1360,15 +1372,10 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
read_only=True
|
read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
available_stock = serializers.FloatField(
|
external_stock = serializers.FloatField(read_only=True, label=_('External Stock'))
|
||||||
label=_('Available Stock'),
|
available_stock = serializers.FloatField(read_only=True, label=_('Available Stock'))
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
available_substitute_stock = serializers.FloatField(read_only=True, label=_('Available Substitute Stock'))
|
available_substitute_stock = serializers.FloatField(read_only=True, label=_('Available Substitute Stock'))
|
||||||
available_variant_stock = serializers.FloatField(read_only=True, label=_('Available Variant Stock'))
|
available_variant_stock = serializers.FloatField(read_only=True, label=_('Available Variant Stock'))
|
||||||
total_available_stock = serializers.FloatField(read_only=True, label=_('Total Available Stock'))
|
|
||||||
external_stock = serializers.FloatField(read_only=True, label=_('External Stock'))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset, build=None):
|
def annotate_queryset(queryset, build=None):
|
||||||
@ -1390,14 +1397,13 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'build',
|
'build',
|
||||||
'bom_item',
|
'bom_item',
|
||||||
'bom_item__part',
|
'bom_item__part',
|
||||||
'bom_item__part__pricing_data',
|
|
||||||
'bom_item__sub_part',
|
'bom_item__sub_part',
|
||||||
'bom_item__sub_part__pricing_data',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-fetch related fields
|
# Pre-fetch related fields
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
'bom_item__sub_part__tags',
|
'allocations',
|
||||||
|
|
||||||
'bom_item__sub_part__stock_items',
|
'bom_item__sub_part__stock_items',
|
||||||
'bom_item__sub_part__stock_items__allocations',
|
'bom_item__sub_part__stock_items__allocations',
|
||||||
'bom_item__sub_part__stock_items__sales_order_allocations',
|
'bom_item__sub_part__stock_items__sales_order_allocations',
|
||||||
@ -1406,21 +1412,58 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'bom_item__substitutes__part__stock_items',
|
'bom_item__substitutes__part__stock_items',
|
||||||
'bom_item__substitutes__part__stock_items__allocations',
|
'bom_item__substitutes__part__stock_items__allocations',
|
||||||
'bom_item__substitutes__part__stock_items__sales_order_allocations',
|
'bom_item__substitutes__part__stock_items__sales_order_allocations',
|
||||||
|
)
|
||||||
|
|
||||||
'allocations',
|
# Defer expensive fields which we do not need for this serializer
|
||||||
'allocations__stock_item',
|
|
||||||
'allocations__stock_item__part',
|
queryset = queryset.defer(
|
||||||
'allocations__stock_item__location',
|
'build__lft',
|
||||||
'allocations__stock_item__location__tags',
|
'build__rght',
|
||||||
'allocations__stock_item__supplier_part',
|
'build__level',
|
||||||
'allocations__stock_item__supplier_part__part',
|
'build__tree_id',
|
||||||
'allocations__stock_item__supplier_part__supplier',
|
'build__destination',
|
||||||
'allocations__stock_item__supplier_part__manufacturer_part',
|
'build__take_from',
|
||||||
'allocations__stock_item__supplier_part__manufacturer_part__manufacturer',
|
'build__completed_by',
|
||||||
|
'build__issued_by',
|
||||||
|
'build__sales_order',
|
||||||
|
'build__parent',
|
||||||
|
'build__notes',
|
||||||
|
'build__metadata',
|
||||||
|
'build__responsible',
|
||||||
|
'build__barcode_data',
|
||||||
|
'build__barcode_hash',
|
||||||
|
'build__project_code',
|
||||||
|
).defer(
|
||||||
|
'bom_item__metadata'
|
||||||
|
).defer(
|
||||||
|
'bom_item__part__lft',
|
||||||
|
'bom_item__part__rght',
|
||||||
|
'bom_item__part__level',
|
||||||
|
'bom_item__part__tree_id',
|
||||||
|
'bom_item__part__tags',
|
||||||
|
'bom_item__part__notes',
|
||||||
|
'bom_item__part__variant_of',
|
||||||
|
'bom_item__part__revision_of',
|
||||||
|
'bom_item__part__creation_user',
|
||||||
|
'bom_item__part__bom_checked_by',
|
||||||
|
'bom_item__part__default_supplier',
|
||||||
|
'bom_item__part__responsible_owner',
|
||||||
|
).defer(
|
||||||
|
'bom_item__sub_part__lft',
|
||||||
|
'bom_item__sub_part__rght',
|
||||||
|
'bom_item__sub_part__level',
|
||||||
|
'bom_item__sub_part__tree_id',
|
||||||
|
'bom_item__sub_part__tags',
|
||||||
|
'bom_item__sub_part__notes',
|
||||||
|
'bom_item__sub_part__variant_of',
|
||||||
|
'bom_item__sub_part__revision_of',
|
||||||
|
'bom_item__sub_part__creation_user',
|
||||||
|
'bom_item__sub_part__bom_checked_by',
|
||||||
|
'bom_item__sub_part__default_supplier',
|
||||||
|
'bom_item__sub_part__responsible_owner',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate the "allocated" quantity
|
# Annotate the "allocated" quantity
|
||||||
# Difficulty: Easy
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
allocated=Coalesce(
|
allocated=Coalesce(
|
||||||
Sum('allocations__quantity'), 0,
|
Sum('allocations__quantity'), 0,
|
||||||
@ -1448,7 +1491,6 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Annotate the "on_order" quantity
|
# Annotate the "on_order" quantity
|
||||||
# Difficulty: Medium
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
on_order=part.filters.annotate_on_order_quantity(reference=ref),
|
on_order=part.filters.annotate_on_order_quantity(reference=ref),
|
||||||
)
|
)
|
||||||
@ -1511,12 +1553,4 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate with the 'total available stock'
|
|
||||||
queryset = queryset.annotate(
|
|
||||||
total_available_stock=ExpressionWrapper(
|
|
||||||
F('available_stock') + F('available_substitute_stock') + F('available_variant_stock'),
|
|
||||||
output_field=FloatField(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -7,7 +7,7 @@ from django.urls import reverse
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from part.models import Part, BomItem
|
from part.models import Part, BomItem
|
||||||
from build.models import Build, BuildItem
|
from build.models import Build, BuildItem, BuildLine
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from build.status_codes import BuildStatus
|
from build.status_codes import BuildStatus
|
||||||
@ -1471,3 +1471,29 @@ class BuildOutputScrapTest(BuildAPITest):
|
|||||||
output.refresh_from_db()
|
output.refresh_from_db()
|
||||||
self.assertEqual(output.status, StockStatus.REJECTED)
|
self.assertEqual(output.status, StockStatus.REJECTED)
|
||||||
self.assertFalse(output.is_building)
|
self.assertFalse(output.is_building)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildLineTests(BuildAPITest):
|
||||||
|
"""Unit tests for the BuildLine API endpoints."""
|
||||||
|
|
||||||
|
def test_filter_available(self):
|
||||||
|
"""Filter BuildLine objects by 'available' status."""
|
||||||
|
|
||||||
|
url = reverse('api-build-line-list')
|
||||||
|
|
||||||
|
# First *all* BuildLine objects
|
||||||
|
response = self.get(url)
|
||||||
|
self.assertEqual(len(response.data), BuildLine.objects.count())
|
||||||
|
|
||||||
|
# Filter by 'available' status
|
||||||
|
# Note: The max_query_time is bumped up here, as postgresql backend has some strange issues (only during testing)
|
||||||
|
response = self.get(url, data={'available': True}, max_query_time=15)
|
||||||
|
n_t = len(response.data)
|
||||||
|
self.assertGreater(n_t, 0)
|
||||||
|
|
||||||
|
# Note: The max_query_time is bumped up here, as postgresql backend has some strange issues (only during testing)
|
||||||
|
response = self.get(url, data={'available': False}, max_query_time=15)
|
||||||
|
n_f = len(response.data)
|
||||||
|
self.assertGreater(n_f, 0)
|
||||||
|
|
||||||
|
self.assertEqual(n_t + n_f, BuildLine.objects.count())
|
||||||
|
@ -1599,6 +1599,7 @@ class BomItemSerializer(
|
|||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
sub_part_detail = kwargs.pop('sub_part_detail', True)
|
sub_part_detail = kwargs.pop('sub_part_detail', True)
|
||||||
pricing = kwargs.pop('pricing', True)
|
pricing = kwargs.pop('pricing', True)
|
||||||
|
substitutes = kwargs.pop('substitutes', True)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -1608,6 +1609,9 @@ class BomItemSerializer(
|
|||||||
if not sub_part_detail:
|
if not sub_part_detail:
|
||||||
self.fields.pop('sub_part_detail', None)
|
self.fields.pop('sub_part_detail', None)
|
||||||
|
|
||||||
|
if not substitutes:
|
||||||
|
self.fields.pop('substitutes', None)
|
||||||
|
|
||||||
if not pricing:
|
if not pricing:
|
||||||
self.fields.pop('pricing_min', None)
|
self.fields.pop('pricing_min', None)
|
||||||
self.fields.pop('pricing_max', None)
|
self.fields.pop('pricing_max', None)
|
||||||
|
@ -2502,7 +2502,10 @@ function renderBuildLineAllocationTable(element, build_line, options={}) {
|
|||||||
|
|
||||||
// Load the allocation items into the table
|
// Load the allocation items into the table
|
||||||
sub_table.bootstrapTable({
|
sub_table.bootstrapTable({
|
||||||
data: build_line.allocations,
|
url: '{% url "api-build-item-list" %}',
|
||||||
|
queryParams: {
|
||||||
|
build_line: build_line.pk,
|
||||||
|
},
|
||||||
showHeader: false,
|
showHeader: false,
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user