2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-02 13:28:49 +00:00

[UI] Stock batch codes (#9145)

* Display batch code in stock item preview

* Show batch code in stock operations modal

* Refactor StockForms

* More table refactoring
This commit is contained in:
Oliver 2025-02-22 19:52:18 +11:00 committed by GitHub
parent 4cacf83294
commit 7098ac74d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 149 additions and 22 deletions

View File

@ -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[];
};

View File

@ -132,15 +132,21 @@ export function TableField({
);
return (
<Table highlightOnHover striped aria-label={`table-field-${field.name}`}>
<Table
highlightOnHover
striped
aria-label={`table-field-${field.name}`}
style={{ width: '100%' }}
>
<Table.Thead>
<Table.Tr>
{definition.headers?.map((header, index) => {
return (
<Table.Th
key={`table-header-${identifierString(header)}-${index}`}
key={`table-header-${identifierString(header.title)}-${index}`}
style={header.style}
>
{header}
{header.title}
</Table.Th>
);
})}

View File

@ -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 (
<RenderInlineModel
{...props}
primary={instance.part_detail?.full_name}
secondary={batch_string}
suffix={<Text size='xs'>{quantity_string}</Text>}
image={instance.part_detail?.thumbnail || instance.part_detail?.image}
url={

View File

@ -276,7 +276,13 @@ export function useCompleteBuildOutputsForm({
<BuildOutputFormRow props={row} record={record} key={record.pk} />
);
},
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({
<BuildOutputFormRow props={row} record={record} key={record.pk} />
);
},
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({
<BuildOutputFormRow props={row} record={record} key={record.pk} />
);
},
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 =

View File

@ -523,9 +523,11 @@ function LineItemFormRow({
onClick={() => open()}
/>
)}
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Flex>
</Table.Td>
<Table.Td>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Table.Td>
</Table.Tr>
{locationOpen && (
<Table.Tr>
@ -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: {

View File

@ -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: {

View File

@ -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) ?? {};

View File

@ -549,6 +549,7 @@ function StockOperationsRow({
<Table.Td>
{record.location ? record.location_detail?.pathstring : '-'}
</Table.Td>
<Table.Td>{record.batch ? record.batch : '-'}</Table.Td>
<Table.Td>
<Group grow justify='space-between' wrap='nowrap'>
<Text>{stockString}</Text>
@ -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' } }
]
}
};

View File

@ -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) => (
<BuildAllocateLineRow props={row} />
),