mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-14 11:05:41 +00:00
[UI] Fix serial and batch code generators (#9772)
* Tweak stock item form - Fix batch code placeholder - Fix serial number placeholder * Tweak build output form * More cleanup * Fix for PurchaseOrderForm * Refactoring placeholder values
This commit is contained in:
@ -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") }}
|
||||
|
@ -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'),
|
||||
|
@ -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<string>('');
|
||||
|
||||
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: <IconTruckDelivery />
|
||||
},
|
||||
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({
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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<number | null>(null);
|
||||
|
||||
const [nextBatchCode, setNextBatchCode] = useState<string>('');
|
||||
const [nextSerialNumber, setNextSerialNumber] = useState<string>('');
|
||||
|
||||
const [expiryDate, setExpiryDate] = useState<string | null>(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({
|
||||
|
@ -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<string, any>;
|
||||
onGenerate?: (value: any) => void;
|
||||
};
|
||||
|
||||
export type GeneratorState = {
|
||||
query: Record<string, any>;
|
||||
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<any>(null);
|
||||
|
||||
@ -29,7 +32,7 @@ export function useGenerator(
|
||||
const [query, setQuery] = useState<Record<string, any>>({});
|
||||
|
||||
// Prevent rapid updates
|
||||
const [debouncedQuery] = useDebouncedValue<Record<string, any>>(query, 250);
|
||||
const [debouncedQuery] = useDebouncedValue<Record<string, any>>(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<string, any>;
|
||||
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<string, any>;
|
||||
onGenerate?: (value: any) => void;
|
||||
}): GeneratorState {
|
||||
return useGenerator({
|
||||
endpoint: ApiEndpoints.generate_serial_number,
|
||||
key: 'serial_number',
|
||||
initialQuery: initialQuery,
|
||||
onGenerate: onGenerate
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -222,7 +222,6 @@ export default function SystemSettings() {
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'SERIAL_NUMBER_GLOBALLY_UNIQUE',
|
||||
'SERIAL_NUMBER_AUTOFILL',
|
||||
'STOCK_DELETE_DEPLETED_DEFAULT',
|
||||
'STOCK_BATCH_CODE_TEMPLATE',
|
||||
'STOCK_ENABLE_EXPIRY',
|
||||
|
@ -167,6 +167,8 @@ test('Build Order - Build Outputs', async ({ browser }) => {
|
||||
.getByLabel('text-field-serial_numbers')
|
||||
.getAttribute('placeholder');
|
||||
|
||||
expect(placeholder).toContain('Next serial number');
|
||||
|
||||
let sn = 1;
|
||||
|
||||
if (!!placeholder && placeholder.includes('Next serial number')) {
|
||||
|
Reference in New Issue
Block a user