From ca34e97ab8a342dc0e83575224d3634154127d0a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 1 Mar 2024 04:13:24 +0000 Subject: [PATCH] Stock location detail --- InvenTree/stock/serializers.py | 10 ++- src/frontend/src/functions/icons.tsx | 4 +- .../src/pages/stock/LocationDetail.tsx | 89 ++++++++++++++++++- src/frontend/src/tables/Details.tsx | 10 +-- 4 files changed, 103 insertions(+), 10 deletions(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index cae8460a41..88ed8e05a2 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -886,6 +886,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): 'pathstring', 'path', 'items', + 'sublocations', 'owner', 'icon', 'custom_icon', @@ -911,13 +912,18 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): def annotate_queryset(queryset): """Annotate extra information to the queryset.""" # Annotate the number of stock items which exist in this category (including subcategories) - queryset = queryset.annotate(items=stock.filters.annotate_location_items()) + queryset = queryset.annotate( + items=stock.filters.annotate_location_items(), + sublocations=stock.filters.annotate_sub_locations(), + ) return queryset url = serializers.CharField(source='get_absolute_url', read_only=True) - items = serializers.IntegerField(read_only=True) + items = serializers.IntegerField(read_only=True, label=_('Stock Items')) + + sublocations = serializers.IntegerField(read_only=True, label=_('Sublocations')) level = serializers.IntegerField(read_only=True) diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index d5106b82b0..3dbd19298f 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -39,6 +39,7 @@ import { IconRulerMeasure, IconShoppingCart, IconShoppingCartHeart, + IconSitemap, IconStack2, IconStatusChange, IconTag, @@ -138,7 +139,8 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } = reference: IconHash, website: IconWorld, email: IconMail, - phone: IconPhone + phone: IconPhone, + sitemap: IconSitemap }; /** diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index a1da38dd23..15edf1ebf6 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -1,14 +1,17 @@ import { t } from '@lingui/macro'; -import { LoadingOverlay, Stack, Text } from '@mantine/core'; -import { IconPackages, IconSitemap } from '@tabler/icons-react'; +import { LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core'; +import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; +import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; import { useInstance } from '../../hooks/UseInstance'; +import { DetailsField, DetailsTable } from '../../tables/Details'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import { StockLocationTable } from '../../tables/stock/StockLocationTable'; @@ -35,8 +38,90 @@ export default function Stock() { } }); + const detailsPanel = useMemo(() => { + if (id && instanceQuery.isFetching) { + return ; + } + + let left: DetailsField[] = [ + { + type: 'text', + name: 'name', + label: t`Name`, + copy: true + }, + { + type: 'text', + name: 'pathstring', + label: t`Path`, + icon: 'sitemap', + copy: true, + hidden: !id + }, + { + type: 'text', + name: 'description', + label: t`Description`, + copy: true + }, + { + type: 'link', + name: 'parent', + model_field: 'name', + icon: 'location', + label: t`Parent Location`, + model: ModelType.stocklocation, + hidden: !location?.parent + } + ]; + + let right: DetailsField[] = [ + { + type: 'text', + name: 'items', + icon: 'stock', + label: t`Stock Items` + }, + { + type: 'text', + name: 'sublocations', + icon: 'location', + label: t`Sublocations`, + hidden: !location?.sublocations + }, + { + type: 'boolean', + name: 'structural', + label: t`Structural`, + icon: 'sitemap' + }, + { + type: 'boolean', + name: 'external', + label: t`External` + } + ]; + + return ( + + {id && location ? ( + + ) : ( + {t`Top level stock location`} + )} + {id && location && } + + ); + }, [location, instanceQuery]); + const locationPanels: PanelType[] = useMemo(() => { return [ + { + name: 'details', + label: t`Location Details`, + icon: , + content: detailsPanel + }, { name: 'stock-items', label: t`Stock Items`, diff --git a/src/frontend/src/tables/Details.tsx b/src/frontend/src/tables/Details.tsx index 05780c8c18..5978dd555a 100644 --- a/src/frontend/src/tables/Details.tsx +++ b/src/frontend/src/tables/Details.tsx @@ -276,13 +276,13 @@ function NameBadge({ pk, type }: { pk: string | number; type: BadgeType }) { * If user is defined, a badge is rendered in addition to main value */ function TableStringValue(props: FieldProps) { - let value = props.field_value; + let value = props?.field_value ?? {}; - if (props.field_data.value_formatter) { + if (props.field_data?.value_formatter) { value = props.field_data.value_formatter(); } - if (props.field_data.badge) { + if (props.field_data?.badge) { return ; } @@ -290,12 +290,12 @@ function TableStringValue(props: FieldProps) {
}> - {value ? value : props.field_data.unit && '0'}{' '} + {value ? value : props.field_data?.unit && '0'}{' '} {props.field_data.unit == true && props.unit} {props.field_data.user && ( - + )}
);