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:
@@ -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
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -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.
|
||||||
|
@@ -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({
|
||||||
|
Reference in New Issue
Block a user