From 0f2675c139cb346edd0fe54aab5898712ecacf76 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 24 Jan 2024 23:33:34 +1100 Subject: [PATCH] [PUI] Child item table (#6334) * Add child stock item table * Fix stock item splitting bug - StockItem tree was not being rebuilt correctly - Add unit tests * Annotate StockItem serializer with "child_items" count * Show or hide "child items" panel * Account for case where tree_id is zero --- InvenTree/stock/api.py | 10 +++++++++ InvenTree/stock/filters.py | 22 ++++++++++++++++++++ InvenTree/stock/serializers.py | 5 +++++ src/frontend/src/pages/stock/StockDetail.tsx | 11 +++++++--- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 1845a3dff7..ac46683243 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -655,6 +655,16 @@ class StockFilter(rest_filters.FilterSet): return queryset.filter(installed_items__gt=0) return queryset.filter(installed_items=0) + has_child_items = rest_filters.BooleanFilter( + label='Has child items', method='filter_has_child_items' + ) + + def filter_has_child_items(self, queryset, name, value): + """Filter stock items by "belongs_to" field being empty.""" + if str2bool(value): + return queryset.filter(child_items__gt=0) + return queryset.filter(child_items=0) + sent_to_customer = rest_filters.BooleanFilter( label='Sent to customer', method='filter_sent_to_customer' ) diff --git a/InvenTree/stock/filters.py b/InvenTree/stock/filters.py index d1292d708c..c992fc05a0 100644 --- a/InvenTree/stock/filters.py +++ b/InvenTree/stock/filters.py @@ -3,6 +3,8 @@ from django.db.models import F, Func, IntegerField, OuterRef, Q, Subquery from django.db.models.functions import Coalesce +from sql_util.utils import SubqueryCount + import stock.models @@ -33,3 +35,23 @@ def annotate_location_items(filter: Q = None): 0, output_field=IntegerField(), ) + + +def annotate_child_items(): + """Construct a queryset annotation which returns the number of children below a certain StockItem node in a StockItem tree.""" + child_stock_query = stock.models.StockItem.objects.filter( + tree_id=OuterRef('tree_id'), + lft__gt=OuterRef('lft'), + rght__lt=OuterRef('rght'), + level__gte=OuterRef('level'), + ) + + return Coalesce( + Subquery( + child_stock_query.annotate( + count=Func(F('pk'), function='COUNT', output_field=IntegerField()) + ).values('count') + ), + 0, + output_field=IntegerField(), + ) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index c38b67f32a..9357346b6e 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -170,6 +170,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): 'allocated', 'expired', 'installed_items', + 'child_items', 'stale', 'tracking_items', 'tags', @@ -288,6 +289,9 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): # Annotate with the total number of "installed items" queryset = queryset.annotate(installed_items=SubqueryCount('installed_parts')) + # Annotate with the total number of "child items" (split stock items) + queryset = queryset.annotate(child_items=stock.filters.annotate_child_items()) + return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) @@ -315,6 +319,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): allocated = serializers.FloatField(required=False) expired = serializers.BooleanField(required=False, read_only=True) installed_items = serializers.IntegerField(read_only=True, required=False) + child_items = serializers.IntegerField(read_only=True, required=False) stale = serializers.BooleanField(required=False, read_only=True) tracking_items = serializers.IntegerField(read_only=True, required=False) diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 3f04626a70..b908cdd5a3 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core'; +import { Alert, LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core'; import { IconBookmark, IconBoxPadding, @@ -34,6 +34,7 @@ import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; +import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { ApiPaths } from '../../enums/ApiEndpoints'; import { useEditStockItem } from '../../forms/StockForms'; @@ -94,14 +95,18 @@ export default function StockDetail() { name: 'installed_items', label: t`Installed Items`, icon: , - content: , hidden: !stockitem?.part_detail?.assembly }, { name: 'child_items', label: t`Child Items`, icon: , - content: + hidden: (stockitem?.child_items ?? 0) == 0, + content: stockitem?.pk ? ( + + ) : ( + + ) }, { name: 'attachments',