mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-30 16:58:06 +00:00
[UI] PO Receive bug (#12709)
* [UI] Fix for receiving items * Add playwright regression test
This commit is contained in:
@@ -375,13 +375,12 @@ function LineItemFormRow({
|
||||
}, [record.destination]);
|
||||
|
||||
// Batch code generator
|
||||
// Note: the generated value is offered as a placeholder (accepted via the
|
||||
// "accept suggested value" button) rather than written into the field
|
||||
// directly - otherwise, a manually-entered batch code can be silently
|
||||
// overwritten if the (debounced, async) generator resolves afterwards
|
||||
const batchCodeGenerator = useBatchCodeGenerator({
|
||||
isEnabled: () => batchOpen,
|
||||
onGenerate: (value: any) => {
|
||||
if (value) {
|
||||
props.changeFn(props.rowId, 'batch_code', value);
|
||||
}
|
||||
}
|
||||
isEnabled: () => batchOpen
|
||||
});
|
||||
|
||||
// Serial number generator
|
||||
@@ -776,7 +775,10 @@ function LineItemFormRow({
|
||||
field_type: 'string',
|
||||
label: t`Batch Code`,
|
||||
description: t`Enter batch code for received items`,
|
||||
value: props.item.batch_code
|
||||
value: props.item.batch_code,
|
||||
placeholderAutofill: true,
|
||||
placeholder:
|
||||
batchCodeGenerator.result && `${batchCodeGenerator.result}`
|
||||
}}
|
||||
error={props.rowErrors?.batch_code?.message}
|
||||
/>
|
||||
@@ -898,7 +900,13 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
||||
return {
|
||||
id: elem.pk,
|
||||
line_item: elem.pk,
|
||||
location: elem.destination ?? elem.destination_detail?.pk ?? null,
|
||||
// Leave unset if this line has no destination of its own, so a
|
||||
// manually-selected location (or the order's own destination
|
||||
// fallback) is actually applied — `destination_detail` already
|
||||
// resolves through that fallback chain, so baking its value in
|
||||
// here would make the backend treat this line as if the user
|
||||
// had explicitly chosen the PO's default, blocking any override.
|
||||
location: elem.destination ?? null,
|
||||
quantity: elem.quantity - elem.received,
|
||||
expiry_date: null,
|
||||
batch_code: '',
|
||||
@@ -938,7 +946,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [filteredItems, records, props, stockStatusCodes]);
|
||||
}, [filteredItems, records, props.orderPk, stockStatusCodes]);
|
||||
|
||||
return useCreateApiFormModal({
|
||||
...props.formProps,
|
||||
|
||||
@@ -119,17 +119,27 @@ export function PurchaseOrderLineItemTable({
|
||||
|
||||
const [singleRecord, setSingleRecord] = useState(null);
|
||||
|
||||
// Keep a stable array reference for unchanged selections, so downstream
|
||||
// memoization isn't defeated by a fresh array literal on every render
|
||||
// (which was resetting in-progress edits in the "receive items" modal)
|
||||
const receiveItems = useMemo(
|
||||
() => (singleRecord ? [singleRecord] : table.selectedRecords),
|
||||
[singleRecord, table.selectedRecords]
|
||||
);
|
||||
|
||||
const onReceiveItemsClose = useCallback(() => {
|
||||
table.clearSelectedRecords();
|
||||
table.refreshTable();
|
||||
// Timeout is a small hack to prevent function being called before re-render
|
||||
setTimeout(() => setSingleRecord(null), 500);
|
||||
}, [table]);
|
||||
|
||||
const receiveLineItems = useReceiveLineItems({
|
||||
items: singleRecord ? [singleRecord] : table.selectedRecords,
|
||||
items: receiveItems,
|
||||
orderPk: orderId,
|
||||
destinationPk: order.destination,
|
||||
formProps: {
|
||||
// Timeout is a small hack to prevent function being called before re-render
|
||||
onClose: () => {
|
||||
table.clearSelectedRecords();
|
||||
table.refreshTable();
|
||||
setTimeout(() => setSingleRecord(null), 500);
|
||||
}
|
||||
onClose: onReceiveItemsClose
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { createApi } from '../api.ts';
|
||||
import { test } from '../baseFixtures.ts';
|
||||
import { readeruser, stevenuser } from '../defaults.ts';
|
||||
import {
|
||||
@@ -607,6 +608,99 @@ test('Purchase Orders - Receive Items', async ({ browser }) => {
|
||||
await page.getByRole('cell', { name: 'my-batch-code' }).first().waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - Custom Location', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await navigate(page, 'purchasing/purchase-order/14/line-items');
|
||||
|
||||
// Line item pk=36 ("Widget Board" / 002.01-PCB) has no destination of its
|
||||
// own, so it falls back to the order's default destination ("Mechanical
|
||||
// Lab"). Target it via its target date, as its quantity gets bumped below
|
||||
// (and its part / IPN are shared with another line on this order).
|
||||
const row = page.getByRole('row').filter({ hasText: '2024-10-23' });
|
||||
await row.waitFor();
|
||||
|
||||
// First, ensure that the row has sufficient quantity to receive
|
||||
// This is required to ensure the robustness of this test,
|
||||
// as the test data may be modified by other tests
|
||||
await row.getByLabel(/row-action-menu-/i).click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
const quantityInput = page.getByRole('textbox', {
|
||||
name: 'number-field-quantity'
|
||||
});
|
||||
const quantity = Number.parseInt(await quantityInput.inputValue());
|
||||
await quantityInput.fill((quantity + 100).toString());
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Item Updated').waitFor();
|
||||
|
||||
// Now, receive a single unit into a location *different* from the
|
||||
// order's default destination ("Mechanical Lab")
|
||||
await row.getByLabel(/row-action-menu-/i).click();
|
||||
await page.getByRole('menuitem', { name: 'Receive line item' }).click();
|
||||
|
||||
await page.getByLabel('tree-field-location').fill('storage room a');
|
||||
await page.getByText('Storage Room A (purple door)').click();
|
||||
|
||||
await page.getByLabel('number-field-quantity').fill('1');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByLabel('action-button-assign-batch-').click();
|
||||
await page
|
||||
.getByLabel('text-field-batch_code', { exact: true })
|
||||
.fill('po-custom-location-test');
|
||||
|
||||
// Short timeout to allow for debouncing
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Items received').waitFor();
|
||||
|
||||
// Verify (via the UI) that the item was received into the location we
|
||||
// picked, and not the order's default destination
|
||||
await loadTab(page, 'Received Stock');
|
||||
await clearTableFilters(page);
|
||||
|
||||
await page
|
||||
.getByRole('textbox', { name: 'table-search-input' })
|
||||
.fill('po-custom-location-test');
|
||||
|
||||
const receivedRow = page
|
||||
.getByRole('row')
|
||||
.filter({ hasText: 'po-custom-location-test' })
|
||||
.first();
|
||||
|
||||
await expect(receivedRow).toContainText('Storage Room A');
|
||||
await expect(receivedRow).not.toContainText('Mechanical Lab');
|
||||
|
||||
// Cross-check against the API, in case the displayed location text does
|
||||
// not reflect the stock item's actual location
|
||||
const api = await createApi({});
|
||||
|
||||
const locations = await api
|
||||
.get('stock/location/', { params: { search: 'Storage Room A' } })
|
||||
.then((res) => res.json());
|
||||
const targetLocation = locations.find(
|
||||
(loc: any) => loc.name === 'Storage Room A'
|
||||
);
|
||||
expect(targetLocation).toBeTruthy();
|
||||
|
||||
const items = await api
|
||||
.get('stock/', { params: { batch: 'po-custom-location-test' } })
|
||||
.then((res) => res.json());
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
|
||||
// The batch code may be shared with stock items received by earlier runs
|
||||
// of this test, so check the most recently-created one
|
||||
const latestItem = items.reduce((a: any, b: any) => (b.pk > a.pk ? b : a));
|
||||
expect(latestItem.location).toBe(targetLocation.pk);
|
||||
|
||||
// This supplier part has a pack quantity of 6, so receiving "1" (pack)
|
||||
// should result in a stock item with a quantity of 6 (base units)
|
||||
expect(latestItem.quantity).toBe(6);
|
||||
});
|
||||
|
||||
test('Purchase Orders - Receive Virtual Items', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
url: 'purchasing/purchase-order/19'
|
||||
|
||||
Reference in New Issue
Block a user