diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py
index 172fdd6f2e..f595266d7d 100644
--- a/src/backend/InvenTree/build/serializers.py
+++ b/src/backend/InvenTree/build/serializers.py
@@ -1519,7 +1519,10 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
'allocations',
'allocations__stock_item',
'allocations__stock_item__part',
+ 'allocations__stock_item__supplier_part',
+ 'allocations__stock_item__supplier_part__manufacturer_part',
'allocations__stock_item__location',
+ 'allocations__stock_item__tags',
'bom_item',
'bom_item__part',
'bom_item__sub_part',
@@ -1603,6 +1606,8 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
location__rght__lte=location.rght,
location__level__gte=location.level,
)
+ else:
+ location = None
# Annotate the "in_production" quantity
queryset = queryset.annotate(
@@ -1623,10 +1628,10 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
reference=ref, filter=stock_filter
),
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(
- reference=ref
+ reference=ref, location=location
),
allocated_to_build_orders=part.filters.annotate_build_order_allocations(
- reference=ref
+ reference=ref, location=location
),
)
diff --git a/src/backend/InvenTree/part/filters.py b/src/backend/InvenTree/part/filters.py
index 460a6c9c64..a7482036dc 100644
--- a/src/backend/InvenTree/part/filters.py
+++ b/src/backend/InvenTree/part/filters.py
@@ -42,7 +42,7 @@ from build.status_codes import BuildStatusGroups
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
-def annotate_in_production_quantity(reference=''):
+def annotate_in_production_quantity(reference: str = '') -> QuerySet:
"""Annotate the 'in production' quantity for each part in a queryset.
- Sum the 'quantity' field for all stock items which are 'in production' for each part.
@@ -63,7 +63,7 @@ def annotate_in_production_quantity(reference=''):
)
-def annotate_scheduled_to_build_quantity(reference: str = ''):
+def annotate_scheduled_to_build_quantity(reference: str = '') -> QuerySet:
"""Annotate the 'scheduled to build' quantity for each part in a queryset.
- This is total scheduled quantity for all build orders which are 'active'
@@ -91,7 +91,7 @@ def annotate_scheduled_to_build_quantity(reference: str = ''):
)
-def annotate_on_order_quantity(reference: str = ''):
+def annotate_on_order_quantity(reference: str = '') -> QuerySet:
"""Annotate the 'on order' quantity for each part in a queryset.
Sum the 'remaining quantity' of each line item for any open purchase orders for each part:
@@ -137,7 +137,7 @@ def annotate_on_order_quantity(reference: str = ''):
)
-def annotate_total_stock(reference: str = '', filter: Q = None):
+def annotate_total_stock(reference: str = '', filter: Q = None) -> QuerySet:
"""Annotate 'total stock' quantity against a queryset.
- This function calculates the 'total stock' for a given part
@@ -161,7 +161,7 @@ def annotate_total_stock(reference: str = '', filter: Q = None):
)
-def annotate_build_order_requirements(reference: str = ''):
+def annotate_build_order_requirements(reference: str = '') -> QuerySet:
"""Annotate the total quantity of each part required for build orders.
- Only interested in 'active' build orders
@@ -179,20 +179,30 @@ def annotate_build_order_requirements(reference: str = ''):
)
-def annotate_build_order_allocations(reference: str = ''):
+def annotate_build_order_allocations(reference: str = '', location=None) -> QuerySet:
"""Annotate the total quantity of each part allocated to build orders.
- This function calculates the total part quantity allocated to open build orders
- Finds all build order allocations for each part (using the provided filter)
- Aggregates the 'allocated quantity' for each relevant build order allocation item
- Args:
+ Arguments:
reference: The relationship reference of the part from the current model
- build_filter: Q object which defines how to filter the allocation items
+ location: If provided, only allocated stock items from this location are considered
"""
# Build filter only returns 'active' build orders
build_filter = Q(build_line__build__status__in=BuildStatusGroups.ACTIVE_CODES)
+ if location is not None:
+ # Filter by location (including any child locations)
+
+ build_filter &= Q(
+ stock_item__location__tree_id=location.tree_id,
+ stock_item__location__lft__gte=location.lft,
+ stock_item__location__rght__lte=location.rght,
+ stock_item__location__level__gte=location.level,
+ )
+
return Coalesce(
SubquerySum(
f'{reference}stock_items__allocations__quantity', filter=build_filter
@@ -202,7 +212,7 @@ def annotate_build_order_allocations(reference: str = ''):
)
-def annotate_sales_order_requirements(reference: str = ''):
+def annotate_sales_order_requirements(reference: str = '') -> QuerySet:
"""Annotate the total quantity of each part required for sales orders.
- Only interested in 'active' sales orders
@@ -222,16 +232,16 @@ def annotate_sales_order_requirements(reference: str = ''):
)
-def annotate_sales_order_allocations(reference: str = ''):
+def annotate_sales_order_allocations(reference: str = '', location=None) -> QuerySet:
"""Annotate the total quantity of each part allocated to sales orders.
- This function calculates the total part quantity allocated to open sales orders"
- Finds all sales order allocations for each part (using the provided filter)
- Aggregates the 'allocated quantity' for each relevant sales order allocation item
- Args:
+ Arguments:
reference: The relationship reference of the part from the current model
- order_filter: Q object which defines how to filter the allocation items
+ location: If provided, only allocated stock items from this location are considered
"""
# Order filter only returns incomplete shipments for open orders
order_filter = Q(
@@ -239,6 +249,16 @@ def annotate_sales_order_allocations(reference: str = ''):
shipment__shipment_date=None,
)
+ if location is not None:
+ # Filter by location (including any child locations)
+
+ order_filter &= Q(
+ item__location__tree_id=location.tree_id,
+ item__location__lft__gte=location.lft,
+ item__location__rght__lte=location.rght,
+ item__location__level__gte=location.level,
+ )
+
return Coalesce(
SubquerySum(
f'{reference}stock_items__sales_order_allocations__quantity',
@@ -249,7 +269,7 @@ def annotate_sales_order_allocations(reference: str = ''):
)
-def variant_stock_query(reference: str = '', filter: Q = None):
+def variant_stock_query(reference: str = '', filter: Q = None) -> QuerySet:
"""Create a queryset to retrieve all stock items for variant parts under the specified part.
- Useful for annotating a queryset with aggregated information about variant parts
@@ -270,7 +290,7 @@ def variant_stock_query(reference: str = '', filter: Q = None):
).filter(stock_filter)
-def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
+def annotate_variant_quantity(subquery: Q, reference: str = 'quantity') -> QuerySet:
"""Create a subquery annotation for all variant part stock items on the given parent query.
Args:
@@ -290,7 +310,7 @@ def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
)
-def annotate_category_parts():
+def annotate_category_parts() -> QuerySet:
"""Construct a queryset annotation which returns the number of parts in a particular category.
- Includes parts in subcategories also
@@ -317,7 +337,7 @@ def annotate_category_parts():
)
-def annotate_default_location(reference=''):
+def annotate_default_location(reference: str = '') -> QuerySet:
"""Construct a queryset that finds the closest default location in the part's category tree.
If the part's category has its own default_location, this is returned.
@@ -340,7 +360,7 @@ def annotate_default_location(reference=''):
)
-def annotate_sub_categories():
+def annotate_sub_categories() -> QuerySet:
"""Construct a queryset annotation which returns the number of subcategories for each provided category."""
subquery = part.models.PartCategory.objects.filter(
tree_id=OuterRef('tree_id'),
@@ -504,7 +524,9 @@ def annotate_bom_item_can_build(queryset: QuerySet, reference: str = '') -> Quer
PARAMETER_FILTER_OPERATORS: list[str] = ['gt', 'gte', 'lt', 'lte', 'ne', 'icontains']
-def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
+def filter_by_parameter(
+ queryset: QuerySet, template_id: int, value: str, func: str = ''
+) -> QuerySet:
"""Filter the given queryset by a given template parameter.
Parts which do not have a value for the given parameter are excluded.
@@ -598,7 +620,9 @@ def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
return queryset.filter(q).distinct()
-def order_by_parameter(queryset, template_id: int, ascending=True):
+def order_by_parameter(
+ queryset: QuerySet, template_id: int, ascending: bool = True
+) -> QuerySet:
"""Order the given queryset by a given template parameter.
Parts which do not have a value for the given parameter are ordered last.
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index 810cc292e6..4e3944a24d 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -46,6 +46,7 @@ import NotesPanel from '../../components/panels/NotesPanel';
import type { PanelType } from '../../components/panels/Panel';
import { PanelGroup } from '../../components/panels/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
+import { RenderStockLocation } from '../../components/render/Stock';
import { useBuildOrderFields } from '../../forms/BuildForms';
import {
useCreateApiFormModal,
@@ -86,6 +87,13 @@ function BuildLinesPanel({
isLoading: boolean;
hasItems: boolean;
}>) {
+ const buildLocation = useInstance({
+ endpoint: ApiEndpoints.stock_location_list,
+ pk: build?.take_from,
+ hasPrimaryKey: true,
+ defaultValue: {}
+ });
+
if (isLoading || !build.pk) {
return ;
}
@@ -94,7 +102,16 @@ function BuildLinesPanel({
return ;
}
- return ;
+ return (
+
+ {buildLocation.instance.pk && (
+ } title={t`Source Location`}>
+
+
+ )}
+
+
+ );
}
function BuildAllocationsPanel({