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',
'allocations__stock_item', 'allocations__stock_item',
'allocations__stock_item__part', 'allocations__stock_item__part',
'allocations__stock_item__supplier_part',
'allocations__stock_item__supplier_part__manufacturer_part',
'allocations__stock_item__location', 'allocations__stock_item__location',
'allocations__stock_item__tags',
'bom_item', 'bom_item',
'bom_item__part', 'bom_item__part',
'bom_item__sub_part', 'bom_item__sub_part',
@@ -1603,6 +1606,8 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
location__rght__lte=location.rght, location__rght__lte=location.rght,
location__level__gte=location.level, location__level__gte=location.level,
) )
else:
location = None
# Annotate the "in_production" quantity # Annotate the "in_production" quantity
queryset = queryset.annotate( queryset = queryset.annotate(
@@ -1623,10 +1628,10 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
reference=ref, filter=stock_filter reference=ref, filter=stock_filter
), ),
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations( 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( 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 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. """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. - 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. """Annotate the 'scheduled to build' quantity for each part in a queryset.
- This is total scheduled quantity for all build orders which are 'active' - 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. """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: 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. """Annotate 'total stock' quantity against a queryset.
- This function calculates the 'total stock' for a given part - 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. """Annotate the total quantity of each part required for build orders.
- Only interested in 'active' 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. """Annotate the total quantity of each part allocated to build orders.
- This function calculates the total part quantity allocated to open 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) - Finds all build order allocations for each part (using the provided filter)
- Aggregates the 'allocated quantity' for each relevant build order allocation item - Aggregates the 'allocated quantity' for each relevant build order allocation item
Args: Arguments:
reference: The relationship reference of the part from the current model 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 only returns 'active' build orders
build_filter = Q(build_line__build__status__in=BuildStatusGroups.ACTIVE_CODES) 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( return Coalesce(
SubquerySum( SubquerySum(
f'{reference}stock_items__allocations__quantity', filter=build_filter 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. """Annotate the total quantity of each part required for sales orders.
- Only interested in 'active' 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. """Annotate the total quantity of each part allocated to sales orders.
- This function calculates the total part quantity allocated to open 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) - Finds all sales order allocations for each part (using the provided filter)
- Aggregates the 'allocated quantity' for each relevant sales order allocation item - Aggregates the 'allocated quantity' for each relevant sales order allocation item
Args: Arguments:
reference: The relationship reference of the part from the current model 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 only returns incomplete shipments for open orders
order_filter = Q( order_filter = Q(
@@ -239,6 +249,16 @@ def annotate_sales_order_allocations(reference: str = ''):
shipment__shipment_date=None, 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( return Coalesce(
SubquerySum( SubquerySum(
f'{reference}stock_items__sales_order_allocations__quantity', 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. """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 - 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) ).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. """Create a subquery annotation for all variant part stock items on the given parent query.
Args: 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. """Construct a queryset annotation which returns the number of parts in a particular category.
- Includes parts in subcategories also - 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. """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. 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.""" """Construct a queryset annotation which returns the number of subcategories for each provided category."""
subquery = part.models.PartCategory.objects.filter( subquery = part.models.PartCategory.objects.filter(
tree_id=OuterRef('tree_id'), 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'] 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. """Filter the given queryset by a given template parameter.
Parts which do not have a value for the given parameter are excluded. 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() 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. """Order the given queryset by a given template parameter.
Parts which do not have a value for the given parameter are ordered last. 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 type { PanelType } from '../../components/panels/Panel';
import { PanelGroup } from '../../components/panels/PanelGroup'; import { PanelGroup } from '../../components/panels/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
import { RenderStockLocation } from '../../components/render/Stock';
import { useBuildOrderFields } from '../../forms/BuildForms'; import { useBuildOrderFields } from '../../forms/BuildForms';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
@@ -86,6 +87,13 @@ function BuildLinesPanel({
isLoading: boolean; isLoading: boolean;
hasItems: boolean; hasItems: boolean;
}>) { }>) {
const buildLocation = useInstance({
endpoint: ApiEndpoints.stock_location_list,
pk: build?.take_from,
hasPrimaryKey: true,
defaultValue: {}
});
if (isLoading || !build.pk) { if (isLoading || !build.pk) {
return <Skeleton w={'100%'} h={400} animate />; return <Skeleton w={'100%'} h={400} animate />;
} }
@@ -94,7 +102,16 @@ function BuildLinesPanel({
return <NoItems />; 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({ function BuildAllocationsPanel({