mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-08 02:02:04 +00:00
Select default location for new stock items if it is set for a part (#12916)
* Select default location for new stock items in the case if it is set for a part Fixes #12304 * Add playwright test to check that the part's default stock location has precedence on the category's default stock location * Rename test cases for clarity in pui_forms.spec.ts * Adjust formatting --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
1 parent
f05b8b44e4
commit
dbffb960f7
2 files changed
+185
-2
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useGlobalSettingsState } from '../states/SettingsStates';
|
||||
import { TagsField } from './CommonFields';
|
||||
|
||||
@@ -23,6 +23,13 @@ export function usePartFields({
|
||||
const [purchaseable, setPurchaseable] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [initialStockLocation, setInitialStockLocation] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const initialStockLocationOverridden = useRef(false);
|
||||
|
||||
const partDefaultLocation = useRef<number | null>(null);
|
||||
const categoryDefaultLocation = useRef<number | null>(null);
|
||||
|
||||
// Set the initial state for the tracked fields based on the global settings
|
||||
useEffect(() => {
|
||||
@@ -35,6 +42,17 @@ export function usePartFields({
|
||||
category: {
|
||||
filters: {
|
||||
structural: false
|
||||
},
|
||||
onValueChange: (_value, category) => {
|
||||
categoryDefaultLocation.current = category?.default_location ?? null;
|
||||
|
||||
// A part-specific default location takes precedence over the
|
||||
// category default location.
|
||||
if (create && !initialStockLocationOverridden.current) {
|
||||
setInitialStockLocation(
|
||||
partDefaultLocation.current ?? categoryDefaultLocation.current
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
name: {},
|
||||
@@ -59,6 +77,17 @@ export function usePartFields({
|
||||
units: {},
|
||||
link: {},
|
||||
default_location: {
|
||||
onValueChange: (value) => {
|
||||
partDefaultLocation.current = value ?? null;
|
||||
|
||||
// Pre-fill the initial stock location, unless the user has already
|
||||
// selected a different location.
|
||||
if (!initialStockLocationOverridden.current) {
|
||||
setInitialStockLocation(
|
||||
partDefaultLocation.current ?? categoryDefaultLocation.current
|
||||
);
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
structural: false
|
||||
}
|
||||
@@ -128,7 +157,13 @@ export function usePartFields({
|
||||
quantity: {
|
||||
value: 0
|
||||
},
|
||||
location: {}
|
||||
location: {
|
||||
value: initialStockLocation,
|
||||
onValueChange: (value) => {
|
||||
initialStockLocationOverridden.current = true;
|
||||
setInitialStockLocation(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -213,6 +248,7 @@ export function usePartFields({
|
||||
partId,
|
||||
virtual,
|
||||
purchaseable,
|
||||
initialStockLocation,
|
||||
create,
|
||||
globalSettings,
|
||||
duplicatePartInstance
|
||||
|
||||
@@ -368,6 +368,153 @@ test('Forms - Initial Stock Field', async ({ browser }) => {
|
||||
await setSettingState({ setting: 'PART_CREATE_INITIAL', value: false });
|
||||
});
|
||||
|
||||
test('Forms - Default Part Location', async ({ browser }) => {
|
||||
// Create a new part and select a the default stock location for the part
|
||||
await setSettingState({ setting: 'PART_CREATE_INITIAL', value: true });
|
||||
|
||||
const partName = `Initial Stock Part ${Date.now()}`;
|
||||
await deletePart(partName);
|
||||
|
||||
const page = await doCachedLogin(browser, {
|
||||
user: stevenuser,
|
||||
url: 'part/category/index/parts'
|
||||
});
|
||||
await page.waitForURL('**/part/category/index/**');
|
||||
|
||||
await page.getByRole('button', { name: 'action-menu-add-parts' }).click();
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'action-menu-add-parts-create-part' })
|
||||
.click();
|
||||
|
||||
await page.getByLabel('text-field-name', { exact: true }).fill(partName);
|
||||
|
||||
await page.getByLabel('tree-field-default_location').fill('production');
|
||||
await page.getByText('Electronics production facility').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Item Created').waitFor();
|
||||
|
||||
// Check that the part's default location is selected when adding a stock item
|
||||
await page
|
||||
.getByLabel('panel-tabs-part')
|
||||
.getByRole('tab', { name: 'Stock', exact: true })
|
||||
.click();
|
||||
|
||||
await page.getByLabel('action-button-add-stock-item').click();
|
||||
|
||||
// The stock item location is rendered via a "tree field" (backed by a real input)
|
||||
const locationField = () =>
|
||||
page.getByRole('textbox', { name: 'tree-field-location' });
|
||||
await expect(locationField()).toHaveValue('Electronics Lab');
|
||||
|
||||
await deletePart(partName);
|
||||
await setSettingState({ setting: 'PART_CREATE_INITIAL', value: false });
|
||||
});
|
||||
|
||||
test('Forms - Default Category Location', async ({ browser }) => {
|
||||
const suffix = Date.now();
|
||||
const categoryName = `Default Location Category ${suffix}`;
|
||||
const partName = `Category Default Location Part ${suffix}`;
|
||||
const api = await createApi({});
|
||||
let categoryId: number | undefined;
|
||||
|
||||
const page = await doCachedLogin(browser, {
|
||||
user: stevenuser,
|
||||
url: 'part/category/index/parts'
|
||||
});
|
||||
|
||||
try {
|
||||
// Create a category with its own default stock location.
|
||||
await loadTab(page, 'Part Categories');
|
||||
await page.getByLabel('action-button-add-part-category').click();
|
||||
await page
|
||||
.getByLabel('text-field-name', { exact: true })
|
||||
.fill(categoryName);
|
||||
await page
|
||||
.getByLabel('text-field-description', { exact: true })
|
||||
.fill('Category default stock location test');
|
||||
await page.getByLabel('tree-field-default_location').fill('production');
|
||||
await page.getByText('Electronics production facility').click();
|
||||
|
||||
const categoryCreated = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('/api/part/category/') &&
|
||||
response.request().method() === 'POST'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
const categoryResponse = await categoryCreated;
|
||||
expect(categoryResponse.status()).toBe(201);
|
||||
const category = await categoryResponse.json();
|
||||
categoryId = category.pk;
|
||||
await page.waitForURL(`**/part/category/${categoryId}/**`);
|
||||
|
||||
// Create a part where the default stock location is not set
|
||||
// so it should inherits the category's on stock creation
|
||||
await loadTab(page, 'Parts');
|
||||
await page.getByRole('button', { name: 'action-menu-add-parts' }).click();
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'action-menu-add-parts-create-part' })
|
||||
.click();
|
||||
await page.getByLabel('text-field-name', { exact: true }).fill(partName);
|
||||
await expect(page.getByLabel('tree-field-category')).toHaveValue(
|
||||
categoryName
|
||||
);
|
||||
await expect(page.getByLabel('tree-field-default_location')).toHaveValue(
|
||||
''
|
||||
);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.waitForURL(/\/part\/\d+\//);
|
||||
const partUrl = page.url();
|
||||
|
||||
await page
|
||||
.getByLabel('panel-tabs-part')
|
||||
.getByRole('tab', { name: 'Stock', exact: true })
|
||||
.click();
|
||||
await page.getByLabel('action-button-add-stock-item').click();
|
||||
|
||||
// Check that the category's default stock location is inherited properly
|
||||
await expect(page.getByLabel('tree-field-location')).toHaveValue(
|
||||
'Electronics Lab'
|
||||
);
|
||||
|
||||
// Check that part-specific default must take precedence over the category default.
|
||||
await page.goto(partUrl);
|
||||
await openDetailAction(page, 'part', 'edit');
|
||||
await expect(page.getByLabel('tree-field-default_location')).toHaveValue(
|
||||
''
|
||||
);
|
||||
|
||||
// Change the part's default stock location to "Electornics Lab/Reel Storage"
|
||||
await page.getByLabel('tree-field-default_location').fill('Reel Storage');
|
||||
await page.getByText('Storage for component reels').click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('Item Updated').waitFor();
|
||||
|
||||
await page
|
||||
.getByLabel('panel-tabs-part')
|
||||
.getByRole('tab', { name: 'Stock', exact: true })
|
||||
.click();
|
||||
await page.getByLabel('action-button-add-stock-item').click();
|
||||
|
||||
// Check that the part's default stock location is overwrite the category's default
|
||||
await expect(page.getByLabel('tree-field-location')).toHaveValue(
|
||||
'Electronics Lab/Reel Storage'
|
||||
);
|
||||
} finally {
|
||||
await deletePart(partName);
|
||||
if (categoryId !== undefined) {
|
||||
const response = await api.delete(`part/category/${categoryId}/`, {
|
||||
data: {
|
||||
delete_child_categories: false,
|
||||
delete_parts: false
|
||||
}
|
||||
});
|
||||
expect(response.status()).toBe(204);
|
||||
}
|
||||
await api.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('Forms - Initial Stock hidden when setting disabled', async ({
|
||||
browser
|
||||
}) => {
|
||||
|
||||
Reference in new issue
Block a user