2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 03:26:45 +00:00

Batch code fix (#9123)

* Fix batch code assignment when receiving items

* Add playwright tests

* Harden playwright tests

* Refactoring
This commit is contained in:
Oliver 2025-02-22 08:34:45 +11:00 committed by GitHub
parent 2521862b1a
commit 1f84f24514
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 129 additions and 63 deletions

View File

@ -88,6 +88,26 @@ export function getStatusCodes(
return statusCodes;
}
/**
* Return a list of status codes select options for a given model type
* returns an array of objects with keys "value" and "display_name"
*
*/
export function getStatusCodeOptions(type: ModelType | string): any[] {
const statusCodes = getStatusCodes(type);
if (!statusCodes) {
return [];
}
return Object.values(statusCodes.values).map((entry) => {
return {
value: entry.key,
display_name: entry.label
};
});
}
/*
* Return the name of a status code, based on the key
*/

View File

@ -22,12 +22,10 @@ import {
IconUser,
IconUsers
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';
import { IconCalendarExclamation } from '@tabler/icons-react';
import dayjs from 'dayjs';
import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
@ -42,6 +40,7 @@ import {
import { Thumbnail } from '../components/images/Thumbnail';
import { ProgressBar } from '../components/items/ProgressBar';
import { StylishText } from '../components/items/StylishText';
import { getStatusCodeOptions } from '../components/render/StatusRenderer';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { InvenTreeIcon } from '../functions/icons';
@ -304,10 +303,14 @@ function LineItemFormRow({
order: record?.order
});
// Generate new serial numbers
serialNumberGenerator.update({
part: record?.supplier_part_detail?.part,
quantity: props.item.quantity
});
if (trackable) {
serialNumberGenerator.update({
part: record?.supplier_part_detail?.part,
quantity: props.item.quantity
});
} else {
props.changeFn(props.idx, 'serial_numbers', undefined);
}
}
});
@ -604,7 +607,10 @@ function LineItemFormRow({
)}
<TableFieldExtraRow
visible={batchOpen}
onValueChange={(value) => props.changeFn(props.idx, 'batch', value)}
onValueChange={(value) => {
props.changeFn(props.idx, 'batch_code', value);
}}
fieldName='batch_code'
fieldDefinition={{
field_type: 'string',
label: t`Batch Code`,
@ -618,6 +624,7 @@ function LineItemFormRow({
onValueChange={(value) =>
props.changeFn(props.idx, 'serial_numbers', value)
}
fieldName='serial_numbers'
fieldDefinition={{
field_type: 'string',
label: t`Serial Numbers`,
@ -632,6 +639,7 @@ function LineItemFormRow({
onValueChange={(value) =>
props.changeFn(props.idx, 'expiry_date', value)
}
fieldName='expiry_date'
fieldDefinition={{
field_type: 'date',
label: t`Expiry Date`,
@ -644,6 +652,7 @@ function LineItemFormRow({
<TableFieldExtraRow
visible={packagingOpen}
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
fieldName='packaging'
fieldDefinition={{
field_type: 'string',
label: t`Packaging`
@ -654,6 +663,7 @@ function LineItemFormRow({
<TableFieldExtraRow
visible={statusOpen}
defaultValue={10}
fieldName='status'
onValueChange={(value) => props.changeFn(props.idx, 'status', value)}
fieldDefinition={{
field_type: 'choice',
@ -665,6 +675,7 @@ function LineItemFormRow({
/>
<TableFieldExtraRow
visible={noteOpen}
fieldName='note'
onValueChange={(value) => props.changeFn(props.idx, 'note', value)}
fieldDefinition={{
field_type: 'string',
@ -689,23 +700,10 @@ type LineItemsForm = {
};
export function useReceiveLineItems(props: LineItemsForm) {
const { data } = useQuery({
queryKey: ['stock', 'status'],
queryFn: async () => {
return api.get(apiUrl(ApiEndpoints.stock_status)).then((response) => {
if (response.status === 200) {
const entries = Object.values(response.data.values);
const mapped = entries.map((item: any) => {
return {
value: item.key,
display_name: item.label
};
});
return mapped;
}
});
}
});
const stockStatusCodes = useMemo(
() => getStatusCodeOptions(ModelType.stockitem),
[]
);
const records = Object.fromEntries(
props.items.map((item) => [item.pk, item])
@ -715,45 +713,47 @@ export function useReceiveLineItems(props: LineItemsForm) {
(elem) => elem.quantity !== elem.received
);
const fields: ApiFormFieldSet = {
id: {
value: props.orderPk,
hidden: true
},
items: {
field_type: 'table',
value: filteredItems.map((elem, idx) => {
return {
line_item: elem.pk,
location: elem.destination ?? elem.destination_detail?.pk ?? null,
quantity: elem.quantity - elem.received,
expiry_date: null,
batch_code: '',
serial_numbers: '',
status: 10,
barcode: null
};
}),
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.line_item];
return (
<LineItemFormRow
props={row}
record={record}
statuses={data}
key={record.pk}
/>
);
const fields: ApiFormFieldSet = useMemo(() => {
return {
id: {
value: props.orderPk,
hidden: true
},
headers: [t`Part`, t`SKU`, t`Received`, t`Quantity`, t`Actions`]
},
location: {
filters: {
structural: false
items: {
field_type: 'table',
value: filteredItems.map((elem, idx) => {
return {
line_item: elem.pk,
location: elem.destination ?? elem.destination_detail?.pk ?? null,
quantity: elem.quantity - elem.received,
expiry_date: null,
batch_code: '',
serial_numbers: '',
status: 10,
barcode: null
};
}),
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.line_item];
return (
<LineItemFormRow
props={row}
record={record}
statuses={stockStatusCodes}
key={record.pk}
/>
);
},
headers: [t`Part`, t`SKU`, t`Received`, t`Quantity`, t`Actions`]
},
location: {
filters: {
structural: false
}
}
}
};
};
}, [filteredItems, props, stockStatusCodes]);
return useCreateApiFormModal({
...props.formProps,
@ -763,6 +763,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
initialData: {
location: props.destinationPk
},
size: '80%'
size: '80%',
successMessage: t`Items received`
});
}

View File

@ -65,6 +65,12 @@ export const getRowFromCell = async (cell) => {
return cell.locator('xpath=ancestor::tr').first();
};
export const clickOnRowMenu = async (cell) => {
const row = await getRowFromCell(cell);
await row.getByLabel(/row-action-menu-/i).click();
};
/**
* Navigate to the provided page, and wait for loading to complete
* @param page
@ -80,6 +86,7 @@ export const navigate = async (page, url: string) => {
}
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle');
};
/**

View File

@ -2,6 +2,7 @@ import { test } from '../baseFixtures.ts';
import {
clearTableFilters,
clickButtonIfVisible,
clickOnRowMenu,
navigate,
openFilterDrawer,
setTableChoiceFilter
@ -257,7 +258,6 @@ test('Purchase Orders - Receive Items', async ({ page }) => {
await page.getByRole('cell', { name: 'PO0014' }).click();
await page.getByRole('tab', { name: 'Order Details' }).click();
await page.getByText('0 / 3').waitFor();
// Select all line items to receive
await page.getByRole('tab', { name: 'Line Items' }).click();
@ -278,4 +278,42 @@ test('Purchase Orders - Receive Items', async ({ page }) => {
await page.getByText('Mechanical Lab').waitFor();
await page.getByRole('button', { name: 'Cancel' }).click();
// Let's actually receive an item (with custom values)
await navigate(page, 'purchasing/purchase-order/2/line-items');
const cell = await page.getByText('Red Paint', { exact: true });
await clickOnRowMenu(cell);
await page.getByRole('menuitem', { name: 'Receive line item' }).click();
// Select destination location
await page.getByLabel('related-field-location').click();
await page.getByRole('option', { name: 'Factory', exact: true }).click();
// Receive only a *single* item
await page.getByLabel('number-field-quantity').fill('1');
// Assign custom information
await page.getByLabel('action-button-assign-batch-').click();
await page.getByLabel('action-button-adjust-packaging').click();
await page.getByLabel('action-button-change-status').click();
await page.getByLabel('action-button-add-note').click();
await page.getByLabel('text-field-batch_code').fill('my-batch-code');
await page.getByLabel('text-field-packaging').fill('bucket');
await page.getByLabel('text-field-note').fill('The quick brown fox');
await page.getByLabel('choice-field-status').click();
await page.getByRole('option', { name: 'Destroyed' }).click();
// Short timeout to allow for debouncing
await page.waitForTimeout(200);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Items received').waitFor();
await page.getByRole('tab', { name: 'Received Stock' }).click();
await clearTableFilters(page);
await page.getByRole('cell', { name: 'my-batch-code' }).first().waitFor();
await page.getByRole('cell', { name: 'bucket' }).first().waitFor();
});