mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +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:
		| @@ -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[]; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
|             ); | ||||
|           })} | ||||
|   | ||||
| @@ -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={ | ||||
|   | ||||
| @@ -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 = | ||||
|   | ||||
| @@ -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: { | ||||
|   | ||||
| @@ -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: { | ||||
|   | ||||
| @@ -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) ?? {}; | ||||
|   | ||||
| @@ -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' } } | ||||
|       ] | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -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} /> | ||||
|       ), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user