2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 06:01:35 +00:00

Build source location (#10220)

* Display build source location

* Fix docstring

* Enhance build availability filter

- Take build source location into account
- Improve pre-fetch

* Enhance type annotations
This commit is contained in:
Oliver
2025-08-25 18:38:22 +10:00
committed by GitHub
parent 564fcc42f2
commit f4233981f5
3 changed files with 68 additions and 22 deletions

View File

@@ -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
),
)

View File

@@ -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.

View File

@@ -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 <Skeleton w={'100%'} h={400} animate />;
}
@@ -94,7 +102,16 @@ function BuildLinesPanel({
return <NoItems />;
}
return <BuildLineTable build={build} />;
return (
<Stack gap='xs'>
{buildLocation.instance.pk && (
<Alert color='blue' icon={<IconSitemap />} title={t`Source Location`}>
<RenderStockLocation instance={buildLocation.instance} />
</Alert>
)}
<BuildLineTable build={build} />
</Stack>
);
}
function BuildAllocationsPanel({