mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +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 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
|
* Return the name of a status code, based on the key
|
||||||
*/
|
*/
|
||||||
|
@ -22,12 +22,10 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconUsers
|
IconUsers
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { IconCalendarExclamation } from '@tabler/icons-react';
|
import { IconCalendarExclamation } from '@tabler/icons-react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { api } from '../App';
|
|
||||||
import { ActionButton } from '../components/buttons/ActionButton';
|
import { ActionButton } from '../components/buttons/ActionButton';
|
||||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||||
@ -42,6 +40,7 @@ import {
|
|||||||
import { Thumbnail } from '../components/images/Thumbnail';
|
import { Thumbnail } from '../components/images/Thumbnail';
|
||||||
import { ProgressBar } from '../components/items/ProgressBar';
|
import { ProgressBar } from '../components/items/ProgressBar';
|
||||||
import { StylishText } from '../components/items/StylishText';
|
import { StylishText } from '../components/items/StylishText';
|
||||||
|
import { getStatusCodeOptions } from '../components/render/StatusRenderer';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { InvenTreeIcon } from '../functions/icons';
|
import { InvenTreeIcon } from '../functions/icons';
|
||||||
@ -304,10 +303,14 @@ function LineItemFormRow({
|
|||||||
order: record?.order
|
order: record?.order
|
||||||
});
|
});
|
||||||
// Generate new serial numbers
|
// Generate new serial numbers
|
||||||
serialNumberGenerator.update({
|
if (trackable) {
|
||||||
part: record?.supplier_part_detail?.part,
|
serialNumberGenerator.update({
|
||||||
quantity: props.item.quantity
|
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
|
<TableFieldExtraRow
|
||||||
visible={batchOpen}
|
visible={batchOpen}
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'batch', value)}
|
onValueChange={(value) => {
|
||||||
|
props.changeFn(props.idx, 'batch_code', value);
|
||||||
|
}}
|
||||||
|
fieldName='batch_code'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'string',
|
field_type: 'string',
|
||||||
label: t`Batch Code`,
|
label: t`Batch Code`,
|
||||||
@ -618,6 +624,7 @@ function LineItemFormRow({
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
props.changeFn(props.idx, 'serial_numbers', value)
|
props.changeFn(props.idx, 'serial_numbers', value)
|
||||||
}
|
}
|
||||||
|
fieldName='serial_numbers'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'string',
|
field_type: 'string',
|
||||||
label: t`Serial Numbers`,
|
label: t`Serial Numbers`,
|
||||||
@ -632,6 +639,7 @@ function LineItemFormRow({
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
props.changeFn(props.idx, 'expiry_date', value)
|
props.changeFn(props.idx, 'expiry_date', value)
|
||||||
}
|
}
|
||||||
|
fieldName='expiry_date'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'date',
|
field_type: 'date',
|
||||||
label: t`Expiry Date`,
|
label: t`Expiry Date`,
|
||||||
@ -644,6 +652,7 @@ function LineItemFormRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={packagingOpen}
|
visible={packagingOpen}
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
|
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
|
||||||
|
fieldName='packaging'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'string',
|
field_type: 'string',
|
||||||
label: t`Packaging`
|
label: t`Packaging`
|
||||||
@ -654,6 +663,7 @@ function LineItemFormRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={statusOpen}
|
visible={statusOpen}
|
||||||
defaultValue={10}
|
defaultValue={10}
|
||||||
|
fieldName='status'
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'status', value)}
|
onValueChange={(value) => props.changeFn(props.idx, 'status', value)}
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'choice',
|
field_type: 'choice',
|
||||||
@ -665,6 +675,7 @@ function LineItemFormRow({
|
|||||||
/>
|
/>
|
||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={noteOpen}
|
visible={noteOpen}
|
||||||
|
fieldName='note'
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'note', value)}
|
onValueChange={(value) => props.changeFn(props.idx, 'note', value)}
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'string',
|
field_type: 'string',
|
||||||
@ -689,23 +700,10 @@ type LineItemsForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useReceiveLineItems(props: LineItemsForm) {
|
export function useReceiveLineItems(props: LineItemsForm) {
|
||||||
const { data } = useQuery({
|
const stockStatusCodes = useMemo(
|
||||||
queryKey: ['stock', 'status'],
|
() => getStatusCodeOptions(ModelType.stockitem),
|
||||||
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 records = Object.fromEntries(
|
const records = Object.fromEntries(
|
||||||
props.items.map((item) => [item.pk, item])
|
props.items.map((item) => [item.pk, item])
|
||||||
@ -715,45 +713,47 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
(elem) => elem.quantity !== elem.received
|
(elem) => elem.quantity !== elem.received
|
||||||
);
|
);
|
||||||
|
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = useMemo(() => {
|
||||||
id: {
|
return {
|
||||||
value: props.orderPk,
|
id: {
|
||||||
hidden: true
|
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
headers: [t`Part`, t`SKU`, t`Received`, t`Quantity`, t`Actions`]
|
items: {
|
||||||
},
|
field_type: 'table',
|
||||||
location: {
|
value: filteredItems.map((elem, idx) => {
|
||||||
filters: {
|
return {
|
||||||
structural: false
|
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({
|
return useCreateApiFormModal({
|
||||||
...props.formProps,
|
...props.formProps,
|
||||||
@ -763,6 +763,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
initialData: {
|
initialData: {
|
||||||
location: props.destinationPk
|
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();
|
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
|
* Navigate to the provided page, and wait for loading to complete
|
||||||
* @param page
|
* @param page
|
||||||
@ -80,6 +86,7 @@ export const navigate = async (page, url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,6 +2,7 @@ import { test } from '../baseFixtures.ts';
|
|||||||
import {
|
import {
|
||||||
clearTableFilters,
|
clearTableFilters,
|
||||||
clickButtonIfVisible,
|
clickButtonIfVisible,
|
||||||
|
clickOnRowMenu,
|
||||||
navigate,
|
navigate,
|
||||||
openFilterDrawer,
|
openFilterDrawer,
|
||||||
setTableChoiceFilter
|
setTableChoiceFilter
|
||||||
@ -257,7 +258,6 @@ test('Purchase Orders - Receive Items', async ({ page }) => {
|
|||||||
await page.getByRole('cell', { name: 'PO0014' }).click();
|
await page.getByRole('cell', { name: 'PO0014' }).click();
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Order Details' }).click();
|
await page.getByRole('tab', { name: 'Order Details' }).click();
|
||||||
await page.getByText('0 / 3').waitFor();
|
|
||||||
|
|
||||||
// Select all line items to receive
|
// Select all line items to receive
|
||||||
await page.getByRole('tab', { name: 'Line Items' }).click();
|
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.getByText('Mechanical Lab').waitFor();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
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