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 { 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 type { UseFormReturnType } from '@mantine/form'; | ||||||
| import { useId } from '@mantine/hooks'; | import { useId } from '@mantine/hooks'; | ||||||
| import { type ReactNode, useCallback, useEffect, useMemo } from 'react'; | import { type ReactNode, useCallback, useEffect, useMemo } from 'react'; | ||||||
| @@ -28,6 +35,12 @@ export type ApiFormFieldChoice = { | |||||||
|   display_name: string; |   display_name: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | // Define individual headers in a table field | ||||||
|  | export type ApiFormFieldHeader = { | ||||||
|  |   title: string; | ||||||
|  |   style?: MantineStyleProp; | ||||||
|  | }; | ||||||
|  |  | ||||||
| /** Definition of the ApiForm field component. | /** Definition of the ApiForm field component. | ||||||
|  * - The 'name' attribute *must* be provided |  * - The 'name' attribute *must* be provided | ||||||
|  * - All other attributes are optional, and may be provided by the API |  * - 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; |   onValueChange?: (value: any, record?: any) => void; | ||||||
|   adjustFilters?: (value: ApiFormAdjustFilterType) => any; |   adjustFilters?: (value: ApiFormAdjustFilterType) => any; | ||||||
|   addRow?: () => any; |   addRow?: () => any; | ||||||
|   headers?: string[]; |   headers?: ApiFormFieldHeader[]; | ||||||
|   depends_on?: string[]; |   depends_on?: string[]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -132,15 +132,21 @@ export function TableField({ | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return ( |   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.Thead> | ||||||
|         <Table.Tr> |         <Table.Tr> | ||||||
|           {definition.headers?.map((header, index) => { |           {definition.headers?.map((header, index) => { | ||||||
|             return ( |             return ( | ||||||
|               <Table.Th |               <Table.Th | ||||||
|                 key={`table-header-${identifierString(header)}-${index}`} |                 key={`table-header-${identifierString(header.title)}-${index}`} | ||||||
|  |                 style={header.style} | ||||||
|               > |               > | ||||||
|                 {header} |                 {header.title} | ||||||
|               </Table.Th> |               </Table.Th> | ||||||
|             ); |             ); | ||||||
|           })} |           })} | ||||||
|   | |||||||
| @@ -63,10 +63,17 @@ export function RenderStockItem( | |||||||
|     quantity_string = `${t`Quantity`}: ${instance.quantity}`; |     quantity_string = `${t`Quantity`}: ${instance.quantity}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   let batch_string = ''; | ||||||
|  |  | ||||||
|  |   if (!!instance.batch) { | ||||||
|  |     batch_string = `${t`Batch`}: ${instance.batch}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <RenderInlineModel |     <RenderInlineModel | ||||||
|       {...props} |       {...props} | ||||||
|       primary={instance.part_detail?.full_name} |       primary={instance.part_detail?.full_name} | ||||||
|  |       secondary={batch_string} | ||||||
|       suffix={<Text size='xs'>{quantity_string}</Text>} |       suffix={<Text size='xs'>{quantity_string}</Text>} | ||||||
|       image={instance.part_detail?.thumbnail || instance.part_detail?.image} |       image={instance.part_detail?.thumbnail || instance.part_detail?.image} | ||||||
|       url={ |       url={ | ||||||
|   | |||||||
| @@ -276,7 +276,13 @@ export function useCompleteBuildOutputsForm({ | |||||||
|             <BuildOutputFormRow props={row} record={record} key={record.pk} /> |             <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: {}, |       status_custom_key: {}, | ||||||
|       location: { |       location: { | ||||||
| @@ -344,7 +350,13 @@ export function useScrapBuildOutputsForm({ | |||||||
|             <BuildOutputFormRow props={row} record={record} key={record.pk} /> |             <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: { |       location: { | ||||||
|         value: location, |         value: location, | ||||||
| @@ -392,7 +404,13 @@ export function useCancelBuildOutputsForm({ | |||||||
|             <BuildOutputFormRow props={row} record={record} key={record.pk} /> |             <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]); |   }, [outputs]); | ||||||
| @@ -522,7 +540,13 @@ export function useAllocateStockToBuildForm({ | |||||||
|       items: { |       items: { | ||||||
|         field_type: 'table', |         field_type: 'table', | ||||||
|         value: [], |         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) => { |         modelRenderer: (row: TableFieldRowProps) => { | ||||||
|           // Find the matching record from the passed 'lineItems' |           // Find the matching record from the passed 'lineItems' | ||||||
|           const record = |           const record = | ||||||
|   | |||||||
| @@ -523,9 +523,11 @@ function LineItemFormRow({ | |||||||
|                 onClick={() => open()} |                 onClick={() => open()} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
|             <RemoveRowButton onClick={() => props.removeFn(props.idx)} /> |  | ||||||
|           </Flex> |           </Flex> | ||||||
|         </Table.Td> |         </Table.Td> | ||||||
|  |         <Table.Td> | ||||||
|  |           <RemoveRowButton onClick={() => props.removeFn(props.idx)} /> | ||||||
|  |         </Table.Td> | ||||||
|       </Table.Tr> |       </Table.Tr> | ||||||
|       {locationOpen && ( |       {locationOpen && ( | ||||||
|         <Table.Tr> |         <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: { |       location: { | ||||||
|         filters: { |         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: { |     location: { | ||||||
|       filters: { |       filters: { | ||||||
|   | |||||||
| @@ -262,7 +262,12 @@ export function useAllocateToSalesOrderForm({ | |||||||
|       items: { |       items: { | ||||||
|         field_type: 'table', |         field_type: 'table', | ||||||
|         value: [], |         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) => { |         modelRenderer: (row: TableFieldRowProps) => { | ||||||
|           const record = |           const record = | ||||||
|             lineItems.find((item) => item.pk == row.item.line_item) ?? {}; |             lineItems.find((item) => item.pk == row.item.line_item) ?? {}; | ||||||
|   | |||||||
| @@ -549,6 +549,7 @@ function StockOperationsRow({ | |||||||
|         <Table.Td> |         <Table.Td> | ||||||
|           {record.location ? record.location_detail?.pathstring : '-'} |           {record.location ? record.location_detail?.pathstring : '-'} | ||||||
|         </Table.Td> |         </Table.Td> | ||||||
|  |         <Table.Td>{record.batch ? record.batch : '-'}</Table.Td> | ||||||
|         <Table.Td> |         <Table.Td> | ||||||
|           <Group grow justify='space-between' wrap='nowrap'> |           <Group grow justify='space-between' wrap='nowrap'> | ||||||
|             <Text>{stockString}</Text> |             <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: { |     location: { | ||||||
|       filters: { |       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: {} |     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: {} |     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: {} |     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: {}, |     status: {}, | ||||||
|     note: {} |     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: { |     location: { | ||||||
|       default: items[0]?.part_detail.default_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: { |     customer: { | ||||||
|       filters: { |       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`, |       description: t`List of entries to choose from`, | ||||||
|       field_type: 'table', |       field_type: 'table', | ||||||
|       value: [], |       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) => ( |       modelRenderer: (row: TableFieldRowProps) => ( | ||||||
|         <BuildAllocateLineRow props={row} /> |         <BuildAllocateLineRow props={row} /> | ||||||
|       ), |       ), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user