2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 04:55:44 +00:00

Batch code generation (#7000)

* Refactor framework for generating batch codes

- Provide additional kwargs to plugin
- Move into new file
- Error handling

* Implement API endpoint for generating a new batch code

* Fixes

* Refactor into stock.generators

* Fix API endpoint

* Pass time context through to plugins

* Generate batch code when receiving items

* Create useGenerator hook

- Build up a dataset and query server whenever it changes
- Look for result in response data
- For now, just used for generating batch codes
- may be used for more in the future

* Refactor PurchaseOrderForms to use new generator hook

* Refactor StockForms implementation

* Remove dead code

* add OAS diff

* fix ref

* fix ref again

* wrong branch, sorry

* Update src/frontend/src/hooks/UseGenerator.tsx

Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com>

* Bump API version

* Do not override batch code if already generated

* Add serial number generator

- Move to /generate/ API endpoint
- Move batch code generator too

* Update PUI endpoints

* Add debouncing to useGenerator hook

* Refactor useGenerator func

* Add serial number generator to stock form

* Add batch code genereator to build order form

* Update buildfields

* Use build batch code when creating new output

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com>
This commit is contained in:
Oliver
2024-05-20 23:56:45 +10:00
committed by GitHub
parent 5cb61d5ad0
commit e93d9c4a74
21 changed files with 513 additions and 64 deletions

View File

@ -108,6 +108,10 @@ export enum ApiEndpoints {
stock_status = 'stock/status/',
stock_install = 'stock/:id/install',
// Generator API endpoints
generate_batch_code = 'generate/batch-code/',
generate_serial_number = 'generate/serial-number/',
// Order API endpoints
purchase_order_list = 'order/po/',
purchase_order_line_list = 'order/po-line/',

View File

@ -19,6 +19,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { InvenTreeIcon } from '../functions/icons';
import { useCreateApiFormModal } from '../hooks/UseForm';
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
import { apiUrl } from '../states/ApiState';
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
@ -34,10 +35,19 @@ export function useBuildOrderFields({
null
);
const [batchCode, setBatchCode] = useState<string>('');
const batchGenerator = useBatchCodeGenerator((value: any) => {
if (!batchCode) {
setBatchCode(value);
}
});
return useMemo(() => {
return {
reference: {},
part: {
disabled: !create,
filters: {
assembly: true,
virtual: false
@ -49,6 +59,10 @@ export function useBuildOrderFields({
record.default_location || record.category_default_location
);
}
batchGenerator.update({
part: value
});
}
},
title: {},
@ -66,7 +80,10 @@ export function useBuildOrderFields({
sales_order: {
icon: <IconTruckDelivery />
},
batch: {},
batch: {
value: batchCode,
onValueChange: (value: any) => setBatchCode(value)
},
target_date: {
icon: <IconCalendar />
},
@ -90,7 +107,7 @@ export function useBuildOrderFields({
}
}
};
}, [create, destination]);
}, [create, destination, batchCode]);
}
export function useBuildOrderOutputFields({

View File

@ -39,6 +39,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { InvenTreeIcon } from '../functions/icons';
import { useCreateApiFormModal } from '../hooks/UseForm';
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
import { apiUrl } from '../states/ApiState';
/*
@ -212,6 +213,12 @@ function LineItemFormRow({
input.changeFn(input.idx, 'location', location);
}, [location]);
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
if (!batchCode) {
setBatchCode(value);
}
});
// State for serializing
const [batchCode, setBatchCode] = useState<string>('');
const [serials, setSerials] = useState<string>('');
@ -219,6 +226,13 @@ function LineItemFormRow({
onClose: () => {
input.changeFn(input.idx, 'batch_code', '');
input.changeFn(input.idx, 'serial_numbers', '');
},
onOpen: () => {
// Generate a new batch code
batchCodeGenerator.update({
part: record?.supplier_part_detail?.part,
order: record?.order
});
}
});

View File

@ -21,6 +21,10 @@ import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../hooks/UseForm';
import {
useBatchCodeGenerator,
useSerialNumberGenerator
} from '../hooks/UseGenerator';
import { apiUrl } from '../states/ApiState';
/**
@ -34,15 +38,41 @@ export function useStockFields({
const [part, setPart] = useState<number | null>(null);
const [supplierPart, setSupplierPart] = useState<number | null>(null);
const [batchCode, setBatchCode] = useState<string>('');
const [serialNumbers, setSerialNumbers] = useState<string>('');
const [trackable, setTrackable] = useState<boolean>(false);
const batchGenerator = useBatchCodeGenerator((value: any) => {
if (!batchCode) {
setBatchCode(value);
}
});
const serialGenerator = useSerialNumberGenerator((value: any) => {
if (!serialNumbers && create && trackable) {
setSerialNumbers(value);
}
});
return useMemo(() => {
const fields: ApiFormFieldSet = {
part: {
value: part,
disabled: !create,
onValueChange: (change) => {
setPart(change);
onValueChange: (value, record) => {
setPart(value);
// TODO: implement remaining functionality from old stock.py
setTrackable(record.trackable ?? false);
batchGenerator.update({ part: value });
serialGenerator.update({ part: value });
if (!record.trackable) {
setSerialNumbers('');
}
// Clear the 'supplier_part' field if the part is changed
setSupplierPart(null);
}
@ -50,7 +80,9 @@ export function useStockFields({
supplier_part: {
// TODO: icon
value: supplierPart,
onValueChange: setSupplierPart,
onValueChange: (value) => {
setSupplierPart(value);
},
filters: {
part_detail: true,
supplier_detail: true,
@ -70,22 +102,29 @@ export function useStockFields({
},
location: {
hidden: !create,
onValueChange: (value) => {
batchGenerator.update({ location: value });
},
filters: {
structural: false
}
// TODO: icon
},
quantity: {
hidden: !create,
description: t`Enter initial quantity for this stock item`
description: t`Enter initial quantity for this stock item`,
onValueChange: (value) => {
batchGenerator.update({ quantity: value });
}
},
serial_numbers: {
// TODO: icon
field_type: 'string',
label: t`Serial Numbers`,
description: t`Enter serial numbers for new stock (or leave blank)`,
required: false,
hidden: !create
disabled: !trackable,
hidden: !create,
value: serialNumbers,
onValueChange: (value) => setSerialNumbers(value)
},
serial: {
hidden: create
@ -93,6 +132,8 @@ export function useStockFields({
},
batch: {
// TODO: icon
value: batchCode,
onValueChange: (value) => setBatchCode(value)
},
status: {},
expiry_date: {
@ -120,7 +161,7 @@ export function useStockFields({
// TODO: refer to stock.py in original codebase
return fields;
}, [part, supplierPart]);
}, [part, supplierPart, batchCode, serialNumbers, trackable, create]);
}
/**

View File

@ -194,6 +194,7 @@ const icons = {
downright: IconCornerDownRight,
barcode: IconQrcode,
barLine: IconMinusVertical,
batch: IconClipboardText,
batch_code: IconClipboardText,
destination: IconFlag,
repeat_destination: IconFlagShare,

View File

@ -0,0 +1,90 @@
import { useDebouncedValue } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { apiUrl } from '../states/ApiState';
export type GeneratorState = {
query: Record<string, any>;
result: any;
update: (params: Record<string, any>, overwrite?: boolean) => void;
};
/* Hook for managing generation of data via the InvenTree API.
* We pass an endpoint, and start with an initially empty query.
* 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 {
// Track the result
const [result, setResult] = useState<any>(null);
// Track the generator query
const [query, setQuery] = useState<Record<string, any>>({});
// Prevent rapid updates
const [debouncedQuery] = useDebouncedValue<Record<string, any>>(query, 250);
// Callback to update the generator query
const update = useCallback(
(params: Record<string, any>, overwrite?: boolean) => {
if (overwrite) {
setQuery(params);
} else {
setQuery((query) => ({
...query,
...params
}));
}
},
[]
);
// API query handler
const queryGenerator = useQuery({
enabled: true,
queryKey: ['generator', key, endpoint, debouncedQuery],
queryFn: async () => {
return api.post(apiUrl(endpoint), debouncedQuery).then((response) => {
const value = response?.data[key];
setResult(value);
if (onGenerate) {
onGenerate(value);
}
return response;
});
}
});
return {
query,
update,
result
};
}
// Generate a batch code with provided data
export function useBatchCodeGenerator(onGenerate: (value: any) => void) {
return useGenerator(
ApiEndpoints.generate_batch_code,
'batch_code',
onGenerate
);
}
// Generate a serial number with provided data
export function useSerialNumberGenerator(onGenerate: (value: any) => void) {
return useGenerator(
ApiEndpoints.generate_serial_number,
'serial_number',
onGenerate
);
}

View File

@ -191,6 +191,13 @@ export default function BuildDetail() {
model: ModelType.stocklocation,
label: t`Destination Location`,
hidden: !build.destination
},
{
type: 'text',
name: 'batch',
label: t`Batch Code`,
hidden: !build.batch,
copy: true
}
];

View File

@ -113,6 +113,9 @@ export default function BuildOutputTable({ build }: { build: any }) {
url: apiUrl(ApiEndpoints.build_output_create, buildId),
title: t`Add Build Output`,
fields: buildOutputFields,
initialData: {
batch_code: build.batch
},
table: table
});