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:
parent
2521862b1a
commit
1f84f24514
@ -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
|
||||
*/
|
||||
|
@ -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`
|
||||
});
|
||||
}
|
||||
|
@ -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');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user