diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 01ea8c661b..d942c68c85 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2703,60 +2703,6 @@ class BomItem(models.Model, DataImportMixin): def get_api_url(): return reverse('api-bom-list') - def available_variant_stock(self): - """ - Returns the total quantity of variant stock available for this BomItem. - - Notes: - - If "allow_variants" is False, this will return zero - - This is used for the API serializer, and is very inefficient - - This logic needs to be converted to a queryset annotation - """ - - # Variant stock is not allowed for this BOM item - if not self.allow_variants: - return 0 - - # Extract a flattened list of part variants - variants = self.sub_part.get_descendants(include_self=False) - - # Calculate 'in_stock' quantity - this is the total current stock count - query = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER) - - query = query.filter( - part__in=variants, - ) - - query = query.aggregate( - in_stock=Coalesce(Sum('quantity'), Decimal(0)) - ) - - in_stock = query['in_stock'] or 0 - - # Calculate the quantity allocated to sales orders - query = OrderModels.SalesOrderAllocation.objects.filter( - line__order__status__in=SalesOrderStatus.OPEN, - shipment__shipment_date=None, - item__part__in=variants, - ).aggregate( - allocated=Coalesce(Sum('quantity'), Decimal(0)), - ) - - sales_order_allocations = query['allocated'] or 0 - - # Calculate the quantity allocated to build orders - query = BuildModels.BuildItem.objects.filter( - build__status__in=BuildStatus.ACTIVE_CODES, - stock_item__part__in=variants, - ).aggregate( - allocated=Coalesce(Sum('quantity'), Decimal(0)), - ) - - build_order_allocations = query['allocated'] or 0 - - available = in_stock - sales_order_allocations - build_order_allocations - - return max(available, 0) def get_valid_parts_for_allocation(self, allow_variants=True, allow_substitutes=True): """ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 0daac6e630..6dc502ae85 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -7,7 +7,9 @@ from decimal import Decimal from django.urls import reverse_lazy from django.db import models, transaction -from django.db.models import ExpressionWrapper, F, Q +from django.db.models import ExpressionWrapper, F, Q, Func +from django.db.models import Subquery, OuterRef, IntegerField, FloatField + from django.db.models.functions import Coalesce from django.utils.translation import ugettext_lazy as _ @@ -577,14 +579,9 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_range = serializers.SerializerMethodField() - # Annotated fields + # Annotated fields for available stock available_stock = serializers.FloatField(read_only=True) available_substitute_stock = serializers.FloatField(read_only=True) - - # Note: 2022-04-15 - # The 'available_variant_stock' field is calculated per-object, - # which means it is very inefficient! - # TODO: This needs to be converted into a query annotation, if possible! available_variant_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): @@ -619,11 +616,18 @@ class BomItemSerializer(InvenTreeModelSerializer): queryset = queryset.prefetch_related('sub_part') queryset = queryset.prefetch_related('sub_part__category') + queryset = queryset.prefetch_related( 'sub_part__stock_items', 'sub_part__stock_items__allocations', 'sub_part__stock_items__sales_order_allocations', ) + + queryset = queryset.prefetch_related( + 'substitutes', + 'substitutes__part__stock_items', + ) + queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks') return queryset @@ -713,7 +717,7 @@ class BomItemSerializer(InvenTreeModelSerializer): ), ) - # Calculate 'available_variant_stock' field + # Calculate 'available_substitute_stock' field queryset = queryset.annotate( available_substitute_stock=ExpressionWrapper( F('substitute_stock') - F('substitute_build_allocations') - F('substitute_sales_allocations'), @@ -721,6 +725,47 @@ class BomItemSerializer(InvenTreeModelSerializer): ) ) + # Annotate the queryset with 'available variant stock' information + variant_stock_query = StockItem.objects.filter( + part__tree_id=OuterRef('sub_part__tree_id'), + part__lft__gt=OuterRef('sub_part__lft'), + part__rght__lt=OuterRef('sub_part__rght'), + ) + + queryset = queryset.alias( + variant_stock_total=Coalesce( + Subquery( + variant_stock_query.annotate( + total=Func(F('quantity'), function='SUM', output_field=FloatField()) + ).values('total')), + 0, + output_field=FloatField() + ), + variant_stock_build_order_allocations=Coalesce( + Subquery( + variant_stock_query.annotate( + total=Func(F('sales_order_allocations__quantity'), function='SUM', output_field=FloatField()), + ).values('total')), + 0, + output_field=FloatField(), + ), + variant_stock_sales_order_allocations=Coalesce( + Subquery( + variant_stock_query.annotate( + total=Func(F('allocations__quantity'), function='SUM', output_field=FloatField()), + ).values('total')), + 0, + output_field=FloatField(), + ) + ) + + queryset = queryset.annotate( + available_variant_stock=ExpressionWrapper( + F('variant_stock_total') - F('variant_stock_build_order_allocations') - F('variant_stock_sales_order_allocations'), + output_field=FloatField(), + ) + ) + return queryset def get_purchase_price_range(self, obj): @@ -797,6 +842,7 @@ class BomItemSerializer(InvenTreeModelSerializer): 'available_stock', 'available_substitute_stock', 'available_variant_stock', + ]