From 871147d93639df704751d5b355d9ce3dc32b1d56 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 30 Aug 2026 11:44:07 +1000 Subject: [PATCH] Stock location fix (#12742) * Fill out stock location based on part default * Add playwright tests --- src/frontend/src/forms/StockForms.tsx | 17 +++++ src/frontend/src/pages/stock/StockDetail.tsx | 1 + .../src/tables/stock/StockItemTable.tsx | 4 +- src/frontend/tests/pages/pui_stock.spec.ts | 62 ++++++++++++++++++- src/frontend/tests/pui_settings.spec.ts | 2 - 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index 68296c7782..c79c5b4f26 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -91,6 +91,7 @@ import { TagsField } from './CommonFields'; */ export function useStockFields({ partId, + locationId, stockItem, create = false, supplierPartId, @@ -98,6 +99,7 @@ export function useStockFields({ modalId }: { partId?: number; + locationId?: number; stockItem?: any; modalId: string; create: boolean; @@ -113,6 +115,9 @@ export function useStockFields({ supplierPartId ?? null ); + // Keep track of the "location" for the new stock item + const [location, setLocation] = useState(locationId ?? null); + const [expiryDate, setExpiryDate] = useState(null); const [quantity, setQuantity] = useState(null); const [purchasePrice, setPurchasePrice] = useState(null); @@ -198,6 +203,15 @@ export function useStockFields({ dayjs().add(expiry_days, 'days').format('YYYY-MM-DD') ); } + + // Fill out the default location for the part, if not already set + setLocation( + (current) => + current ?? + record?.default_location ?? + record?.category_default_location ?? + null + ); } }, supplier_part: { @@ -227,7 +241,9 @@ export function useStockFields({ location: { // Cannot adjust location for existing stock items hidden: !create, + value: location, onValueChange: (value) => { + setLocation(value); batchGenerator.update({ location: value }); }, filters: { @@ -323,6 +339,7 @@ export function useStockFields({ partId, globalSettings, supplierPart, + location, create, supplierPartId, purchasePrice, diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 2e93f01ba4..904551af81 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -374,6 +374,7 @@ export default function StockDetail() { const duplicateStockItemFields = useStockFields({ create: true, + locationId: stockitem.location, modalId: 'duplicate-stock-item' }); diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 007022bb84..6a3a39bcc4 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -412,6 +412,7 @@ export function StockItemTable({ const newStockItemFields = useStockFields({ create: true, partId: params.part, + locationId: params.location, supplierPartId: params.supplier_part, pricing: params.pricing, modalId: 'add-stock-item' @@ -423,8 +424,7 @@ export function StockItemTable({ modalId: 'add-stock-item', fields: newStockItemFields, initialData: { - part: params.part, - location: params.location + part: params.part }, follow: params.openNewStockItem ?? true, table: table, diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index 6912e8a90a..c8c23bd182 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -658,6 +658,64 @@ test('Stock - Disassembly', async ({ browser }) => { await page.getByRole('cell', { name: 'Thumbnail XT90-F' }).waitFor(); await page.getByRole('cell', { name: 'Thumbnail XT90-M' }).waitFor(); - - await page.waitForTimeout(2000); +}); + +// Test that the "location" field is correctly pre-filled when creating new stock items +test('Stock - Default Location', async ({ browser }) => { + const page = await doCachedLogin(browser, { + url: 'stock/location/12/stock-items' + }); + + // The stock item location is rendered via a "tree field" (backed by a real input) + const locationField = () => + page.getByRole('textbox', { name: 'tree-field-location' }); + + // Scenario 1: Creating a new stock item from within a location should default to that location + await page + .getByRole('button', { name: 'action-button-add-stock-item' }) + .click(); + await expect(locationField()).toHaveValue('Location 0'); + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Scenario 2: Duplicating a stock item should retain its original location + await navigate(page, 'stock/item/2/details'); + await page + .getByRole('button', { name: 'action-menu-stock-item-actions' }) + .click(); + await page + .getByRole('menuitem', { name: 'action-menu-stock-item-actions-duplicate' }) + .click(); + await expect(locationField()).toHaveValue('Electronics Lab/Reel Storage'); + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Scenario 3: Creating a new stock item for a part with a specified default location + // First, assign a default location to the part + await navigate(page, 'part/86/details'); + await page.getByText('1553WDBK').first().waitFor(); + await page.keyboard.press('Control+E'); + await page.getByPlaceholder('Select location').fill('Mechanical Lab'); + await page + .getByRole('listbox') + .getByText('Mechanical Lab', { exact: true }) + .click(); + await page.waitForTimeout(250); + + // The "Submit" button is only enabled if the value actually changed - if a + // previous test run already left this part with the same default location, + // there is nothing to save, so just close the form instead + const submitButton = page.getByRole('button', { name: 'Submit' }); + if (await submitButton.isEnabled()) { + await submitButton.click(); + await page.waitForLoadState('networkidle'); + } else { + await page.getByRole('button', { name: 'Cancel' }).click(); + } + + // Now, create a new stock item for this part, and check the location is pre-filled + await navigate(page, 'part/86/stock'); + await page + .getByRole('button', { name: 'action-button-add-stock-item' }) + .click(); + await expect(locationField()).toHaveValue('Factory/Mechanical Lab'); + await page.getByRole('button', { name: 'Cancel' }).click(); }); diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts index 0f3a7a866b..47edcbfcd6 100644 --- a/src/frontend/tests/pui_settings.spec.ts +++ b/src/frontend/tests/pui_settings.spec.ts @@ -428,8 +428,6 @@ test('Settings - Admin - Barcode History', async ({ browser }) => { for (const barcode of barcodes) { await checkBarcode(barcode); } - - await page.waitForTimeout(2500); }); test('Settings - Admin - Parameter', async ({ browser }) => {