diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index 9b58e2c48b..57700c61bf 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -197,7 +197,6 @@ Configuration of stock item options | Name | Description | Default | Units | | ---- | ----------- | ------- | ----- | {{ globalsetting("SERIAL_NUMBER_GLOBALLY_UNIQUE") }} -{{ globalsetting("SERIAL_NUMBER_AUTOFILL") }} {{ globalsetting("STOCK_DELETE_DEPLETED_DEFAULT") }} {{ globalsetting("STOCK_BATCH_CODE_TEMPLATE") }} {{ globalsetting("STOCK_ENABLE_EXPIRY") }} diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index 2a20685afc..c823a96184 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -640,12 +640,6 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, - 'SERIAL_NUMBER_AUTOFILL': { - 'name': _('Autofill Serial Numbers'), - 'description': _('Autofill serial numbers in forms'), - 'default': False, - 'validator': bool, - }, 'STOCK_DELETE_DEPLETED_DEFAULT': { 'name': _('Delete Depleted Stock'), 'description': _('Determines default behavior when a stock item is depleted'), diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index 0ff3241a09..9c32092058 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -25,8 +25,10 @@ import { import { ProgressBar } from '../components/items/ProgressBar'; import { StatusRenderer } from '../components/render/StatusRenderer'; import { useCreateApiFormModal } from '../hooks/UseForm'; -import { useBatchCodeGenerator } from '../hooks/UseGenerator'; -import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder'; +import { + useBatchCodeGenerator, + useSerialNumberGenerator +} from '../hooks/UseGenerator'; import { useGlobalSettingsState } from '../states/SettingsState'; import { PartColumn } from '../tables/ColumnRenderers'; @@ -44,9 +46,9 @@ export function useBuildOrderFields({ const [batchCode, setBatchCode] = useState(''); - const batchGenerator = useBatchCodeGenerator((value: any) => { - if (!batchCode) { - setBatchCode(value); + const batchGenerator = useBatchCodeGenerator({ + onGenerate: (value: any) => { + setBatchCode((batch: any) => batch || value); } }); @@ -96,6 +98,9 @@ export function useBuildOrderFields({ icon: }, batch: { + placeholder: + batchGenerator.result && + `${t`Next batch code`}: ${batchGenerator.result}`, value: batchCode, onValueChange: (value: any) => setBatchCode(value) }, @@ -143,7 +148,7 @@ export function useBuildOrderFields({ } return fields; - }, [create, destination, batchCode, globalSettings]); + }, [create, destination, batchCode, batchGenerator.result, globalSettings]); } export function useBuildOrderOutputFields({ @@ -170,10 +175,17 @@ export function useBuildOrderOutputFields({ setQuantity(Math.max(0, build_quantity - build_complete)); }, [build]); - const serialPlaceholder = useSerialNumberPlaceholder({ - partId: build.part_detail?.pk, - key: 'build-output', - enabled: build.part_detail?.trackable + const serialGenerator = useSerialNumberGenerator({ + initialQuery: { + part: build.part || build.part_detail?.pk + } + }); + + const batchGenerator = useBatchCodeGenerator({ + initialQuery: { + part: build.part || build.part_detail?.pk, + quantity: build.quantity + } }); return useMemo(() => { @@ -186,9 +198,15 @@ export function useBuildOrderOutputFields({ }, serial_numbers: { hidden: !trackable, - placeholder: serialPlaceholder + placeholder: + serialGenerator.result && + `${t`Next serial number`}: ${serialGenerator.result}` + }, + batch_code: { + placeholder: + batchGenerator.result && + `${t`Next batch code`}: ${batchGenerator.result}` }, - batch_code: {}, location: { value: location, onValueChange: (value: any) => { @@ -199,7 +217,7 @@ export function useBuildOrderOutputFields({ hidden: !trackable } }; - }, [quantity, serialPlaceholder, trackable]); + }, [quantity, batchGenerator.result, serialGenerator.result, trackable]); } function BuildOutputFormRow({ diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index ca1a9e2452..f95e762089 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -295,16 +295,20 @@ function LineItemFormRow({ }, [record.destination]); // Batch code generator - const batchCodeGenerator = useBatchCodeGenerator((value: any) => { - if (value) { - props.changeFn(props.idx, 'batch_code', value); + const batchCodeGenerator = useBatchCodeGenerator({ + onGenerate: (value: any) => { + if (value) { + props.changeFn(props.idx, 'batch_code', value); + } } }); // Serial number generator - const serialNumberGenerator = useSerialNumberGenerator((value: any) => { - if (value) { - props.changeFn(props.idx, 'serial_numbers', value); + const serialNumberGenerator = useSerialNumberGenerator({ + onGenerate: (value: any) => { + if (value) { + props.changeFn(props.idx, 'serial_numbers', value); + } } }); @@ -478,8 +482,10 @@ function LineItemFormRow({ fieldDefinition={{ field_type: 'number', value: props.item.quantity, - onValueChange: (value) => - props.changeFn(props.idx, 'quantity', value) + onValueChange: (value) => { + props.changeFn(props.idx, 'quantity', value); + serialNumberGenerator.update({ quantity: value }); + } }} error={props.rowErrors?.quantity?.message} /> diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index 1f2f5c5e2b..d4c6cb0317 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -56,7 +56,6 @@ import { useBatchCodeGenerator, useSerialNumberGenerator } from '../hooks/UseGenerator'; -import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder'; import { useGlobalSettingsState } from '../states/SettingsState'; import { StatusFilterOptions } from '../tables/Filter'; @@ -79,35 +78,20 @@ export function useStockFields({ const [supplierPart, setSupplierPart] = useState(null); - const [nextBatchCode, setNextBatchCode] = useState(''); - const [nextSerialNumber, setNextSerialNumber] = useState(''); - const [expiryDate, setExpiryDate] = useState(null); - const batchGenerator = useBatchCodeGenerator((value: any) => { - if (value) { - setNextBatchCode(`${t`Next batch code`}: ${value}`); - } else { - setNextBatchCode(''); + const batchGenerator = useBatchCodeGenerator({ + initialQuery: { + part: partInstance?.pk || partId } }); - const serialGenerator = useSerialNumberGenerator((value: any) => { - if (value) { - setNextSerialNumber(`${t`Next serial number`}: ${value}`); - } else { - setNextSerialNumber(''); + const serialGenerator = useSerialNumberGenerator({ + initialQuery: { + part: partInstance?.pk || partId } }); - useEffect(() => { - if (partInstance?.pk) { - // Update the generators whenever the part ID changes - batchGenerator.update({ part: partInstance.pk }); - serialGenerator.update({ part: partInstance.pk }); - } - }, [partInstance.pk]); - return useMemo(() => { const fields: ApiFormFieldSet = { part: { @@ -181,16 +165,23 @@ export function useStockFields({ description: t`Enter serial numbers for new stock (or leave blank)`, required: false, hidden: !create, - placeholder: nextSerialNumber + placeholder: + serialGenerator.result && + `${t`Next serial number`}: ${serialGenerator.result}` }, serial: { + placeholder: + serialGenerator.result && + `${t`Next serial number`}: ${serialGenerator.result}`, hidden: create || partInstance.trackable == false || (stockItem?.quantity != undefined && stockItem?.quantity != 1) }, batch: { - placeholder: nextBatchCode + placeholder: + batchGenerator.result && + `${t`Next batch code`}: ${batchGenerator.result}` }, status_custom_key: { label: t`Stock Status` @@ -234,8 +225,8 @@ export function useStockFields({ partId, globalSettings, supplierPart, - nextSerialNumber, - nextBatchCode, + serialGenerator.result, + batchGenerator.result, create ]); } @@ -332,21 +323,23 @@ export function useStockItemSerializeFields({ partId: number; trackable: boolean; }) { - const snPlaceholder = useSerialNumberPlaceholder({ - partId: partId, - key: 'stock-item-serialize', - enabled: trackable + const serialGenerator = useSerialNumberGenerator({ + initialQuery: { + part: partId + } }); return useMemo(() => { return { quantity: {}, serial_numbers: { - placeholder: snPlaceholder + placeholder: + serialGenerator.result && + `${t`Next serial number`}: ${serialGenerator.result}` }, destination: {} }; - }, [snPlaceholder]); + }, [serialGenerator.result]); } function StockItemDefaultMove({ diff --git a/src/frontend/src/hooks/UseGenerator.tsx b/src/frontend/src/hooks/UseGenerator.tsx index aaaca15862..c923a2c0e1 100644 --- a/src/frontend/src/hooks/UseGenerator.tsx +++ b/src/frontend/src/hooks/UseGenerator.tsx @@ -6,6 +6,13 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; import { api } from '../App'; +export type GeneratorProps = { + endpoint: ApiEndpoints; + key: string; + initialQuery?: Record; + onGenerate?: (value: any) => void; +}; + export type GeneratorState = { query: Record; result: any; @@ -17,11 +24,7 @@ export type GeneratorState = { * We can pass additional parameters to the query, and update the query as needed. * Each update calls a new query to the API, and the result is stored in the state. */ -export function useGenerator( - endpoint: ApiEndpoints, - key: string, - onGenerate?: (value: any) => void -): GeneratorState { +export function useGenerator(props: GeneratorProps): GeneratorState { // Track the result const [result, setResult] = useState(null); @@ -29,7 +32,7 @@ export function useGenerator( const [query, setQuery] = useState>({}); // Prevent rapid updates - const [debouncedQuery] = useDebouncedValue>(query, 250); + const [debouncedQuery] = useDebouncedValue>(query, 100); // Callback to update the generator query const update = useCallback( @@ -42,27 +45,44 @@ export function useGenerator( ...params })); } - - queryGenerator.refetch(); }, [] ); // API query handler const queryGenerator = useQuery({ - enabled: false, - queryKey: ['generator', key, endpoint, debouncedQuery], + enabled: true, + queryKey: [ + 'generator', + props.key, + props.endpoint, + props.initialQuery, + debouncedQuery + ], + refetchOnMount: false, + refetchOnWindowFocus: false, queryFn: async () => { - return api.post(apiUrl(endpoint), debouncedQuery).then((response) => { - const value = response?.data[key]; - setResult(value); + const generatorQuery = { + ...debouncedQuery, + ...(props.initialQuery ?? {}) + }; - if (onGenerate) { - onGenerate(value); - } + return api + .post(apiUrl(props.endpoint), generatorQuery) + .then((response) => { + const value = response?.data[props.key]; + setResult(value); - return response; - }); + props.onGenerate?.(value); + + return response; + }) + .catch((error) => { + console.error( + `Error generating ${props.key} @ ${props.endpoint}:`, + error + ); + }); } }); @@ -74,19 +94,33 @@ export function useGenerator( } // Generate a batch code with provided data -export function useBatchCodeGenerator(onGenerate: (value: any) => void) { - return useGenerator( - ApiEndpoints.generate_batch_code, - 'batch_code', - onGenerate - ); +export function useBatchCodeGenerator({ + initialQuery, + onGenerate +}: { + initialQuery?: Record; + onGenerate?: (value: any) => void; +}): GeneratorState { + return useGenerator({ + endpoint: ApiEndpoints.generate_batch_code, + key: 'batch_code', + initialQuery: initialQuery, + onGenerate: onGenerate + }); } // Generate a serial number with provided data -export function useSerialNumberGenerator(onGenerate: (value: any) => void) { - return useGenerator( - ApiEndpoints.generate_serial_number, - 'serial_number', - onGenerate - ); +export function useSerialNumberGenerator({ + initialQuery, + onGenerate +}: { + initialQuery?: Record; + onGenerate?: (value: any) => void; +}): GeneratorState { + return useGenerator({ + endpoint: ApiEndpoints.generate_serial_number, + key: 'serial_number', + initialQuery: initialQuery, + onGenerate: onGenerate + }); } diff --git a/src/frontend/src/hooks/UsePlaceholder.tsx b/src/frontend/src/hooks/UsePlaceholder.tsx deleted file mode 100644 index 3a655862c4..0000000000 --- a/src/frontend/src/hooks/UsePlaceholder.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { t } from '@lingui/core/macro'; -import { useQuery } from '@tanstack/react-query'; -import { useMemo } from 'react'; - -import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; -import { apiUrl } from '@lib/functions/Api'; -import { api } from '../App'; - -/** - * Hook for generating a placeholder text for a serial number input - * - * This hook fetches the latest serial number information for a given part and generates a placeholder string. - * - * @param partId The ID of the part to fetch serial number information for - * @param key A unique key to identify the query - * @param enabled Whether the query should be enabled - */ -export function useSerialNumberPlaceholder({ - partId, - key, - enabled = true -}: { - partId: number; - key: string; - enabled?: boolean; -}): string | undefined { - // Fetch serial number information (if available) - const snQuery = useQuery({ - queryKey: ['serial-placeholder', key, partId], - enabled: enabled ?? true, - queryFn: async () => { - if (!partId) { - return null; - } - - const url = apiUrl(ApiEndpoints.part_serial_numbers, partId); - - return api - .get(url) - .then((response) => { - if (response.status === 200) { - return response.data; - } else { - return null; - } - }) - .catch(() => { - return null; - }); - } - }); - - const placeholder = useMemo(() => { - if (!enabled) { - return undefined; - } else if (snQuery.data?.next) { - return `${t`Next serial number`}: ${snQuery.data.next}`; - } else if (snQuery.data?.latest) { - return `${t`Latest serial number`}: ${snQuery.data.latest}`; - } else { - return undefined; - } - }, [enabled, snQuery.data]); - - return placeholder; -} diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index b268ec65dd..4d1ed7a2bd 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -222,7 +222,6 @@ export default function SystemSettings() { { .getByLabel('text-field-serial_numbers') .getAttribute('placeholder'); + expect(placeholder).toContain('Next serial number'); + let sn = 1; if (!!placeholder && placeholder.includes('Next serial number')) {