From 7098ac74d2228377970dc1e5ee66d9aa2d4f5572 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 22 Feb 2025 19:52:18 +1100 Subject: [PATCH] [UI] Stock batch codes (#9145) * Display batch code in stock item preview * Show batch code in stock operations modal * Refactor StockForms * More table refactoring --- .../components/forms/fields/ApiFormField.tsx | 17 ++++- .../components/forms/fields/TableField.tsx | 12 +++- src/frontend/src/components/render/Stock.tsx | 7 ++ src/frontend/src/forms/BuildForms.tsx | 32 +++++++-- src/frontend/src/forms/PurchaseOrderForms.tsx | 13 +++- src/frontend/src/forms/ReturnOrderForms.tsx | 7 +- src/frontend/src/forms/SalesOrderForms.tsx | 7 +- src/frontend/src/forms/StockForms.tsx | 69 ++++++++++++++++--- .../src/forms/selectionListFields.tsx | 7 +- 9 files changed, 149 insertions(+), 22 deletions(-) diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index aa78ec2d42..b12dc97cee 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -1,5 +1,12 @@ import { t } from '@lingui/macro'; -import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core'; +import { + Alert, + FileInput, + type MantineStyleProp, + NumberInput, + Stack, + Switch +} from '@mantine/core'; import type { UseFormReturnType } from '@mantine/form'; import { useId } from '@mantine/hooks'; import { type ReactNode, useCallback, useEffect, useMemo } from 'react'; @@ -28,6 +35,12 @@ export type ApiFormFieldChoice = { display_name: string; }; +// Define individual headers in a table field +export type ApiFormFieldHeader = { + title: string; + style?: MantineStyleProp; +}; + /** Definition of the ApiForm field component. * - The 'name' attribute *must* be provided * - All other attributes are optional, and may be provided by the API @@ -103,7 +116,7 @@ export type ApiFormFieldType = { onValueChange?: (value: any, record?: any) => void; adjustFilters?: (value: ApiFormAdjustFilterType) => any; addRow?: () => any; - headers?: string[]; + headers?: ApiFormFieldHeader[]; depends_on?: string[]; }; diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index 6f395f78b9..a49a13c35a 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -132,15 +132,21 @@ export function TableField({ ); return ( - +
{definition.headers?.map((header, index) => { return ( - {header} + {header.title} ); })} diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index a45395e3ed..4365e137b1 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -63,10 +63,17 @@ export function RenderStockItem( quantity_string = `${t`Quantity`}: ${instance.quantity}`; } + let batch_string = ''; + + if (!!instance.batch) { + batch_string = `${t`Batch`}: ${instance.batch}`; + } + return ( {quantity_string}} image={instance.part_detail?.thumbnail || instance.part_detail?.image} url={ diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index 787cb2a16b..87cde86203 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -276,7 +276,13 @@ export function useCompleteBuildOutputsForm({ ); }, - headers: [t`Part`, t`Build Output`, t`Batch`, t`Status`] + headers: [ + { title: t`Part` }, + { title: t`Build Output` }, + { title: t`Batch` }, + { title: t`Status` }, + { title: '', style: { width: '50px' } } + ] }, status_custom_key: {}, location: { @@ -344,7 +350,13 @@ export function useScrapBuildOutputsForm({ ); }, - headers: [t`Part`, t`Stock Item`, t`Batch`, t`Status`] + headers: [ + { title: t`Part` }, + { title: t`Stock Item` }, + { title: t`Batch` }, + { title: t`Status` }, + { title: '', style: { width: '50px' } } + ] }, location: { value: location, @@ -392,7 +404,13 @@ export function useCancelBuildOutputsForm({ ); }, - headers: [t`Part`, t`Stock Item`, t`Batch`, t`Status`] + headers: [ + { title: t`Part` }, + { title: t`Stock Item` }, + { title: t`Batch` }, + { title: t`Status` }, + { title: '', style: { width: '50px' } } + ] } }; }, [outputs]); @@ -522,7 +540,13 @@ export function useAllocateStockToBuildForm({ items: { field_type: 'table', value: [], - headers: [t`Part`, t`Allocated`, t`Stock Item`, t`Quantity`], + headers: [ + { title: t`Part`, style: { minWidth: '175px' } }, + { title: t`Allocated`, style: { minWidth: '175px' } }, + { title: t`Stock Item`, style: { width: '100%' } }, + { title: t`Quantity`, style: { minWidth: '175px' } }, + { title: '', style: { width: '50px' } } + ], modelRenderer: (row: TableFieldRowProps) => { // Find the matching record from the passed 'lineItems' const record = diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index caea7ddb90..5812578a7b 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -523,9 +523,11 @@ function LineItemFormRow({ onClick={() => open()} /> )} - props.removeFn(props.idx)} /> + + props.removeFn(props.idx)} /> + {locationOpen && ( @@ -745,7 +747,14 @@ export function useReceiveLineItems(props: LineItemsForm) { /> ); }, - headers: [t`Part`, t`SKU`, t`Received`, t`Quantity`, t`Actions`] + headers: [ + { title: t`Part`, style: { minWidth: '200px' } }, + { title: t`SKU`, style: { minWidth: '200px' } }, + { title: t`Received`, style: { minWidth: '200px' } }, + { title: t`Quantity`, style: { width: '200px' } }, + { title: t`Actions` }, + { title: '', style: { width: '50px' } } + ] }, location: { filters: { diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index f4e2776e54..df9d88e47d 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -234,7 +234,12 @@ export function useReceiveReturnOrderLineItems( /> ); }, - headers: [t`Part`, t`Quantity`, t`Status`] + headers: [ + { title: t`Part`, style: { minWidth: '250px' } }, + { title: t`Quantity`, style: { minWidth: '250px' } }, + { title: t`Status`, style: { minWidth: '250px' } }, + { title: '', style: { width: '50px' } } + ] }, location: { filters: { diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index c30f9ea0db..b1d893d34e 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -262,7 +262,12 @@ export function useAllocateToSalesOrderForm({ items: { field_type: 'table', value: [], - headers: [t`Part`, t`Allocated`, t`Stock Item`, t`Quantity`], + headers: [ + { title: t`Part`, style: { minWidth: '200px' } }, + { title: t`Allocated`, style: { minWidth: '200px' } }, + { title: t`Stock Item`, style: { width: '100%' } }, + { title: t`Quantity`, style: { width: '200px' } } + ], modelRenderer: (row: TableFieldRowProps) => { const record = lineItems.find((item) => item.pk == row.item.line_item) ?? {}; diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index eb6facefc0..a2bc692674 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -549,6 +549,7 @@ function StockOperationsRow({ {record.location ? record.location_detail?.pathstring : '-'} + {record.batch ? record.batch : '-'} {stockString} @@ -697,7 +698,14 @@ function stockTransferFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`Stock`, t`Move`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`Stock` }, + { title: t`Move`, style: { width: '200px' } }, + { title: t`Actions` } + ] }, location: { filters: { @@ -734,7 +742,14 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Remove`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: t`Remove`, style: { width: '200px' } }, + { title: t`Actions` } + ] }, notes: {} }; @@ -766,7 +781,14 @@ function stockAddFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Add`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: t`Add`, style: { width: '200px' } }, + { title: t`Actions` } + ] }, notes: {} }; @@ -795,7 +817,14 @@ function stockCountFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Count`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: t`Count`, style: { width: '200px' } }, + { title: t`Actions` } + ] }, notes: {} }; @@ -826,7 +855,13 @@ function stockChangeStatusFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: '', style: { width: '50px' } } + ] }, status: {}, note: {} @@ -862,7 +897,13 @@ function stockMergeFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: t`Actions` } + ] }, location: { default: items[0]?.part_detail.default_location, @@ -904,7 +945,13 @@ function stockAssignFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: '', style: { width: '50px' } } + ] }, customer: { filters: { @@ -942,7 +989,13 @@ function stockDeleteFields(items: any[]): ApiFormFieldSet { /> ); }, - headers: [t`Part`, t`Location`, t`In Stock`, t`Actions`] + headers: [ + { title: t`Part` }, + { title: t`Location` }, + { title: t`Batch` }, + { title: t`In Stock` }, + { title: '', style: { width: '50px' } } + ] } }; diff --git a/src/frontend/src/forms/selectionListFields.tsx b/src/frontend/src/forms/selectionListFields.tsx index f13de38161..f08c5a9b0d 100644 --- a/src/frontend/src/forms/selectionListFields.tsx +++ b/src/frontend/src/forms/selectionListFields.tsx @@ -100,7 +100,12 @@ export function selectionListFields(): ApiFormFieldSet { description: t`List of entries to choose from`, field_type: 'table', value: [], - headers: [t`Value`, t`Label`, t`Description`, t`Active`], + headers: [ + { title: t`Value` }, + { title: t`Label` }, + { title: t`Description` }, + { title: t`Active` } + ], modelRenderer: (row: TableFieldRowProps) => ( ),