mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 12:05:53 +00:00
[Refactor] Custom states (#8438)
* Enhancements for "custom state" form
- More intuitive form actions
* Improve back-end validation
* Improve table rendering
* Fix lookup for useStatusCodes
* Fix status display for SockDetail page
* Fix SalesOrder status display
* Refactor get_custom_classes
- Add StatusCode.custom_values method
* Fix for status table filters
* Cleanup (and note to self)
* Include custom state values in specific API endpoints
* Add serializer class definition
* Use same serializer for AllStatusView
* Fix API to match existing frontend type StatusCodeListInterface
* Enable filtering by reference status type
* Add option to duplicate an existing custom state
* Improved validation for the InvenTreeCustomUserStateModel class
* Code cleanup
* Fix default value in StockOperationsRow
* Use custom status values in stock operations
* Allow custom values
* Fix migration
* Bump API version
* Fix filtering of stock items by "status"
* Enhance status filter for orders
* Fix status code rendering
* Build Order API filter
* Update playwright tests for build filters
* Additional playwright tests for stock table filters
* Add 'custom' attribute
* Fix unit tests
* Add custom state field validation
* Implement StatusCodeMixin for setting status code values
* Clear out 'custom key' if the base key does not match
* Updated playwright testing
* Remove timeout
* Refactor detail pages which display status
* Update old migrations - add field validator
* Remove dead code
* Simplify API query filtering
* Revert "Simplify API query filtering"
This reverts commit 06c858ae7c
.
* Fix save method
* Unit test fixes
* Fix for ReturnOrderLineItem
* Reorganize code
* Adjust unit test
This commit is contained in:
@ -1,19 +1,22 @@
|
||||
import { Badge, Center, type MantineSize } from '@mantine/core';
|
||||
|
||||
import { colorMap } from '../../defaults/backendMappings';
|
||||
import { statusColorMap } from '../../defaults/backendMappings';
|
||||
import type { ModelType } from '../../enums/ModelType';
|
||||
import { resolveItem } from '../../functions/conversion';
|
||||
import { useGlobalStatusState } from '../../states/StatusState';
|
||||
|
||||
interface StatusCodeInterface {
|
||||
key: string;
|
||||
export interface StatusCodeInterface {
|
||||
key: number;
|
||||
label: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface StatusCodeListInterface {
|
||||
[key: string]: StatusCodeInterface;
|
||||
status_class: string;
|
||||
values: {
|
||||
[key: string]: StatusCodeInterface;
|
||||
};
|
||||
}
|
||||
|
||||
interface RenderStatusLabelOptionsInterface {
|
||||
@ -33,10 +36,10 @@ function renderStatusLabel(
|
||||
let color = null;
|
||||
|
||||
// Find the entry which matches the provided key
|
||||
for (const name in codes) {
|
||||
const entry = codes[name];
|
||||
for (const name in codes.values) {
|
||||
const entry: StatusCodeInterface = codes.values[name];
|
||||
|
||||
if (entry.key == key) {
|
||||
if (entry?.key == key) {
|
||||
text = entry.label;
|
||||
color = entry.color;
|
||||
break;
|
||||
@ -51,7 +54,7 @@ function renderStatusLabel(
|
||||
|
||||
// Fallbacks
|
||||
if (color == null) color = 'default';
|
||||
color = colorMap[color] || colorMap['default'];
|
||||
color = statusColorMap[color] || statusColorMap['default'];
|
||||
const size = options.size || 'xs';
|
||||
|
||||
if (!text) {
|
||||
@ -65,7 +68,9 @@ function renderStatusLabel(
|
||||
);
|
||||
}
|
||||
|
||||
export function getStatusCodes(type: ModelType | string) {
|
||||
export function getStatusCodes(
|
||||
type: ModelType | string
|
||||
): StatusCodeListInterface | null {
|
||||
const statusCodeList = useGlobalStatusState.getState().status;
|
||||
|
||||
if (statusCodeList === undefined) {
|
||||
@ -97,7 +102,7 @@ export function getStatusCodeName(
|
||||
}
|
||||
|
||||
for (const name in statusCodes) {
|
||||
const entry = statusCodes[name];
|
||||
const entry: StatusCodeInterface = statusCodes.values[name];
|
||||
|
||||
if (entry.key == key) {
|
||||
return entry.name;
|
||||
|
@ -20,7 +20,7 @@ export const statusCodeList: Record<string, ModelType> = {
|
||||
/*
|
||||
* Map the colors used in the backend to the colors used in the frontend
|
||||
*/
|
||||
export const colorMap: { [key: string]: string } = {
|
||||
export const statusColorMap: { [key: string]: string } = {
|
||||
dark: 'dark',
|
||||
warning: 'yellow',
|
||||
success: 'green',
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { IconUsers } from '@tabler/icons-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
||||
import type {
|
||||
StatusCodeInterface,
|
||||
StatusCodeListInterface
|
||||
} from '../components/render/StatusRenderer';
|
||||
import { useGlobalStatusState } from '../states/StatusState';
|
||||
|
||||
export function projectCodeFields(): ApiFormFieldSet {
|
||||
return {
|
||||
@ -12,16 +18,51 @@ export function projectCodeFields(): ApiFormFieldSet {
|
||||
};
|
||||
}
|
||||
|
||||
export function customStateFields(): ApiFormFieldSet {
|
||||
return {
|
||||
key: {},
|
||||
name: {},
|
||||
label: {},
|
||||
color: {},
|
||||
logical_key: {},
|
||||
model: {},
|
||||
reference_status: {}
|
||||
};
|
||||
export function useCustomStateFields(): ApiFormFieldSet {
|
||||
// Status codes
|
||||
const statusCodes = useGlobalStatusState();
|
||||
|
||||
// Selected base status class
|
||||
const [statusClass, setStatusClass] = useState<string>('');
|
||||
|
||||
// Construct a list of status options based on the selected status class
|
||||
const statusOptions: any[] = useMemo(() => {
|
||||
const options: any[] = [];
|
||||
|
||||
const valuesList = Object.values(statusCodes.status ?? {}).find(
|
||||
(value: StatusCodeListInterface) => value.status_class === statusClass
|
||||
);
|
||||
|
||||
Object.values(valuesList?.values ?? {}).forEach(
|
||||
(value: StatusCodeInterface) => {
|
||||
options.push({
|
||||
value: value.key,
|
||||
display_name: value.label
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return options;
|
||||
}, [statusCodes, statusClass]);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
reference_status: {
|
||||
onValueChange(value) {
|
||||
setStatusClass(value);
|
||||
}
|
||||
},
|
||||
logical_key: {
|
||||
field_type: 'choice',
|
||||
choices: statusOptions
|
||||
},
|
||||
key: {},
|
||||
name: {},
|
||||
label: {},
|
||||
color: {},
|
||||
model: {}
|
||||
};
|
||||
}, [statusOptions]);
|
||||
}
|
||||
|
||||
export function customUnitsFields(): ApiFormFieldSet {
|
||||
|
@ -482,7 +482,7 @@ function StockOperationsRow({
|
||||
|
||||
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
||||
onOpen: () => {
|
||||
setStatus(record?.status || undefined);
|
||||
setStatus(record?.status_custom_key || record?.status || undefined);
|
||||
props.changeFn(props.idx, 'status', record?.status || undefined);
|
||||
},
|
||||
onClose: () => {
|
||||
|
@ -31,14 +31,18 @@ export default function useStatusCodes({
|
||||
const statusCodeList = useGlobalStatusState.getState().status;
|
||||
|
||||
const codes = useMemo(() => {
|
||||
const statusCodes = getStatusCodes(modelType) || {};
|
||||
const statusCodes = getStatusCodes(modelType) || null;
|
||||
|
||||
const codesMap: Record<any, any> = {};
|
||||
|
||||
for (const name in statusCodes) {
|
||||
codesMap[name] = statusCodes[name].key;
|
||||
if (!statusCodes) {
|
||||
return codesMap;
|
||||
}
|
||||
|
||||
Object.keys(statusCodes.values).forEach((name) => {
|
||||
codesMap[name] = statusCodes.values[name].key;
|
||||
});
|
||||
|
||||
return codesMap;
|
||||
}, [modelType, statusCodeList]);
|
||||
|
||||
|
@ -107,6 +107,15 @@ export default function BuildDetail() {
|
||||
label: t`Status`,
|
||||
model: ModelType.build
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.build,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!build.status_custom_key || build.status_custom_key == build.status
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
|
@ -144,6 +144,15 @@ export default function PurchaseOrderDetail() {
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.purchaseorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.purchaseorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -115,6 +115,15 @@ export default function ReturnOrderDetail() {
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.returnorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.returnorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -124,6 +124,15 @@ export default function SalesOrderDetail() {
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.salesorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.salesorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -133,11 +133,21 @@ export default function StockDetail() {
|
||||
icon: 'part',
|
||||
hidden: !part.IPN
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.stockitem
|
||||
},
|
||||
{
|
||||
name: 'status_custom_key',
|
||||
type: 'status',
|
||||
label: t`Stock Status`,
|
||||
model: ModelType.stockitem
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.stockitem,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!stockitem.status_custom_key ||
|
||||
stockitem.status_custom_key == stockitem.status
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
@ -845,11 +855,10 @@ export default function StockDetail() {
|
||||
key='batch'
|
||||
/>,
|
||||
<StatusRenderer
|
||||
status={stockitem.status_custom_key}
|
||||
status={stockitem.status_custom_key || stockitem.status}
|
||||
type={ModelType.stockitem}
|
||||
options={{
|
||||
size: 'lg',
|
||||
hidden: !!stockitem.status_custom_key
|
||||
size: 'lg'
|
||||
}}
|
||||
key='status'
|
||||
/>,
|
||||
|
@ -9,7 +9,7 @@ import type { ModelType } from '../enums/ModelType';
|
||||
import { apiUrl } from './ApiState';
|
||||
import { useUserState } from './UserState';
|
||||
|
||||
type StatusLookup = Record<ModelType | string, StatusCodeListInterface>;
|
||||
export type StatusLookup = Record<ModelType | string, StatusCodeListInterface>;
|
||||
|
||||
interface ServerStateProps {
|
||||
status?: StatusLookup;
|
||||
@ -35,8 +35,10 @@ export const useGlobalStatusState = create<ServerStateProps>()(
|
||||
.then((response) => {
|
||||
const newStatusLookup: StatusLookup = {} as StatusLookup;
|
||||
for (const key in response.data) {
|
||||
newStatusLookup[statusCodeList[key] || key] =
|
||||
response.data[key].values;
|
||||
newStatusLookup[statusCodeList[key] || key] = {
|
||||
status_class: key,
|
||||
values: response.data[key].values
|
||||
};
|
||||
}
|
||||
set({ status: newStatusLookup });
|
||||
})
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import type {
|
||||
StatusCodeInterface,
|
||||
StatusCodeListInterface
|
||||
} from '../components/render/StatusRenderer';
|
||||
import type { ModelType } from '../enums/ModelType';
|
||||
import { useGlobalStatusState } from '../states/StatusState';
|
||||
import { type StatusLookup, useGlobalStatusState } from '../states/StatusState';
|
||||
|
||||
/**
|
||||
* Interface for the table filter choice
|
||||
@ -71,17 +75,18 @@ export function StatusFilterOptions(
|
||||
model: ModelType
|
||||
): () => TableFilterChoice[] {
|
||||
return () => {
|
||||
const statusCodeList = useGlobalStatusState.getState().status;
|
||||
const statusCodeList: StatusLookup | undefined =
|
||||
useGlobalStatusState.getState().status;
|
||||
|
||||
if (!statusCodeList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const codes = statusCodeList[model];
|
||||
const codes: StatusCodeListInterface | undefined = statusCodeList[model];
|
||||
|
||||
if (codes) {
|
||||
return Object.keys(codes).map((key) => {
|
||||
const entry = codes[key];
|
||||
return Object.keys(codes.values).map((key) => {
|
||||
const entry: StatusCodeInterface = codes.values[key];
|
||||
return {
|
||||
value: entry.key.toString(),
|
||||
label: entry.label?.toString() ?? entry.key.toString()
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Badge } from '@mantine/core';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import type {
|
||||
StatusCodeInterface,
|
||||
StatusCodeListInterface
|
||||
} from '../../components/render/StatusRenderer';
|
||||
import { statusColorMap } from '../../defaults/backendMappings';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { customStateFields } from '../../forms/CommonForms';
|
||||
import { useCustomStateFields } from '../../forms/CommonForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
@ -12,10 +18,17 @@ import {
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useGlobalStatusState } from '../../states/StatusState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import type { TableColumn } from '../Column';
|
||||
import type { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { type RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
import {
|
||||
type RowAction,
|
||||
RowDeleteAction,
|
||||
RowDuplicateAction,
|
||||
RowEditAction
|
||||
} from '../RowActions';
|
||||
|
||||
/**
|
||||
* Table for displaying list of custom states
|
||||
@ -23,12 +36,64 @@ import { type RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
export default function CustomStateTable() {
|
||||
const table = useTable('customstates');
|
||||
|
||||
const statusCodes = useGlobalStatusState();
|
||||
|
||||
// Find the associated logical state key
|
||||
const getLogicalState = useCallback(
|
||||
(group: string, key: number) => {
|
||||
const valuesList = Object.values(statusCodes.status ?? {}).find(
|
||||
(value: StatusCodeListInterface) => value.status_class === group
|
||||
);
|
||||
|
||||
const value = Object.values(valuesList?.values ?? {}).find(
|
||||
(value: StatusCodeInterface) => value.key === key
|
||||
);
|
||||
|
||||
return value?.label ?? value?.name ?? '';
|
||||
},
|
||||
[statusCodes]
|
||||
);
|
||||
|
||||
const user = useUserState();
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'reference_status',
|
||||
label: t`Status Group`,
|
||||
field_type: 'choice',
|
||||
choices: Object.values(statusCodes.status ?? {}).map(
|
||||
(value: StatusCodeListInterface) => ({
|
||||
label: value.status_class,
|
||||
value: value.status_class
|
||||
})
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [statusCodes]);
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'reference_status',
|
||||
title: t`Status`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'logical_key',
|
||||
title: t`Logical State`,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
const stateText = getLogicalState(
|
||||
record.reference_status,
|
||||
record.logical_key
|
||||
);
|
||||
return stateText ? stateText : record.logical_key;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'name',
|
||||
title: t`Identifier`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
@ -36,34 +101,45 @@ export default function CustomStateTable() {
|
||||
title: t`Display Name`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'color'
|
||||
},
|
||||
{
|
||||
accessor: 'key',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'logical_key',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'model_name',
|
||||
title: t`Model`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'reference_status',
|
||||
title: t`Status`,
|
||||
sortable: true
|
||||
accessor: 'color',
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Badge
|
||||
color={statusColorMap[record.color] || statusColorMap['default']}
|
||||
variant='filled'
|
||||
size='xs'
|
||||
>
|
||||
{record.color}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [getLogicalState]);
|
||||
|
||||
const newCustomStateFields = useCustomStateFields();
|
||||
const duplicateCustomStateFields = useCustomStateFields();
|
||||
const editCustomStateFields = useCustomStateFields();
|
||||
|
||||
const [initialStateData, setInitialStateData] = useState<any>({});
|
||||
|
||||
const newCustomState = useCreateApiFormModal({
|
||||
url: ApiEndpoints.custom_state_list,
|
||||
title: t`Add State`,
|
||||
fields: customStateFields(),
|
||||
fields: newCustomStateFields,
|
||||
table: table
|
||||
});
|
||||
|
||||
const duplicateCustomState = useCreateApiFormModal({
|
||||
url: ApiEndpoints.custom_state_list,
|
||||
title: t`Add State`,
|
||||
fields: duplicateCustomStateFields,
|
||||
initialData: initialStateData,
|
||||
table: table
|
||||
});
|
||||
|
||||
@ -75,7 +151,7 @@ export default function CustomStateTable() {
|
||||
url: ApiEndpoints.custom_state_list,
|
||||
pk: selectedCustomState,
|
||||
title: t`Edit State`,
|
||||
fields: customStateFields(),
|
||||
fields: editCustomStateFields,
|
||||
table: table
|
||||
});
|
||||
|
||||
@ -96,6 +172,13 @@ export default function CustomStateTable() {
|
||||
editCustomState.open();
|
||||
}
|
||||
}),
|
||||
RowDuplicateAction({
|
||||
hidden: !user.hasAddRole(UserRoles.admin),
|
||||
onClick: () => {
|
||||
setInitialStateData(record);
|
||||
duplicateCustomState.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
hidden: !user.hasDeleteRole(UserRoles.admin),
|
||||
onClick: () => {
|
||||
@ -112,7 +195,10 @@ export default function CustomStateTable() {
|
||||
return [
|
||||
<AddItemButton
|
||||
key={'add'}
|
||||
onClick={() => newCustomState.open()}
|
||||
onClick={() => {
|
||||
setInitialStateData({});
|
||||
newCustomState.open();
|
||||
}}
|
||||
tooltip={t`Add State`}
|
||||
/>
|
||||
];
|
||||
@ -122,6 +208,7 @@ export default function CustomStateTable() {
|
||||
<>
|
||||
{newCustomState.modal}
|
||||
{editCustomState.modal}
|
||||
{duplicateCustomState.modal}
|
||||
{deleteCustomState.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.custom_state_list)}
|
||||
@ -130,6 +217,7 @@ export default function CustomStateTable() {
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
enableDownload: true
|
||||
}}
|
||||
/>
|
||||
|
@ -34,7 +34,7 @@ export const clickButtonIfVisible = async (page, name, timeout = 500) => {
|
||||
export const clearTableFilters = async (page) => {
|
||||
await openFilterDrawer(page);
|
||||
await clickButtonIfVisible(page, 'Clear Filters');
|
||||
await page.getByLabel('filter-drawer-close').click();
|
||||
await closeFilterDrawer(page);
|
||||
};
|
||||
|
||||
export const setTableChoiceFilter = async (page, filter, value) => {
|
||||
@ -42,7 +42,9 @@ export const setTableChoiceFilter = async (page, filter, value) => {
|
||||
|
||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||
await page.getByPlaceholder('Select filter').fill(filter);
|
||||
await page.getByRole('option', { name: 'Status' }).click();
|
||||
await page.getByPlaceholder('Select filter').click();
|
||||
await page.getByRole('option', { name: filter }).click();
|
||||
|
||||
await page.getByPlaceholder('Select filter value').click();
|
||||
await page.getByRole('option', { name: value }).click();
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { test } from '../baseFixtures.ts';
|
||||
import { baseUrl } from '../defaults.ts';
|
||||
import {
|
||||
clickButtonIfVisible,
|
||||
clearTableFilters,
|
||||
getRowFromCell,
|
||||
openFilterDrawer
|
||||
setTableChoiceFilter
|
||||
} from '../helpers.ts';
|
||||
import { doQuickLogin } from '../login.ts';
|
||||
|
||||
@ -266,6 +266,24 @@ test('Build Order - Filters', async ({ page }) => {
|
||||
|
||||
await page.goto(`${baseUrl}/manufacturing/index/buildorders`);
|
||||
|
||||
await openFilterDrawer(page);
|
||||
await clickButtonIfVisible(page, 'Clear Filters');
|
||||
await clearTableFilters(page);
|
||||
await page.getByText('1 - 24 / 24').waitFor();
|
||||
|
||||
// Toggle 'Outstanding' filter
|
||||
await setTableChoiceFilter(page, 'Outstanding', 'Yes');
|
||||
await page.getByText('1 - 18 / 18').waitFor();
|
||||
await clearTableFilters(page);
|
||||
await setTableChoiceFilter(page, 'Outstanding', 'No');
|
||||
await page.getByText('1 - 6 / 6').waitFor();
|
||||
await clearTableFilters(page);
|
||||
|
||||
// Filter by custom status code
|
||||
await setTableChoiceFilter(page, 'Status', 'Pending Approval');
|
||||
|
||||
// Single result - navigate through to the build order
|
||||
await page.getByText('1 - 1 / 1').waitFor();
|
||||
await page.getByRole('cell', { name: 'BO0023' }).click();
|
||||
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByText('Pending Approval').first().waitFor();
|
||||
});
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { test } from '../baseFixtures.js';
|
||||
import { baseUrl } from '../defaults.js';
|
||||
import { clickButtonIfVisible, openFilterDrawer } from '../helpers.js';
|
||||
import {
|
||||
clearTableFilters,
|
||||
clickButtonIfVisible,
|
||||
openFilterDrawer,
|
||||
setTableChoiceFilter
|
||||
} from '../helpers.js';
|
||||
import { doQuickLogin } from '../login.js';
|
||||
|
||||
test('Stock - Basic Tests', async ({ page }) => {
|
||||
@ -84,9 +89,15 @@ test('Stock - Filters', async ({ page }) => {
|
||||
.getByRole('cell', { name: 'A round table - with blue paint' })
|
||||
.waitFor();
|
||||
|
||||
// Clear filters (ready for next set of tests)
|
||||
await openFilterDrawer(page);
|
||||
await clickButtonIfVisible(page, 'Clear Filters');
|
||||
// Filter by custom status code
|
||||
await clearTableFilters(page);
|
||||
await setTableChoiceFilter(page, 'Status', 'Incoming goods inspection');
|
||||
await page.getByText('1 - 8 / 8').waitFor();
|
||||
await page.getByRole('cell', { name: '1551AGY' }).first().waitFor();
|
||||
await page.getByRole('cell', { name: 'widget.blue' }).first().waitFor();
|
||||
await page.getByRole('cell', { name: '002.01-PCBA' }).first().waitFor();
|
||||
|
||||
await clearTableFilters(page);
|
||||
});
|
||||
|
||||
test('Stock - Serial Numbers', async ({ page }) => {
|
||||
@ -158,47 +169,58 @@ test('Stock - Serial Numbers', async ({ page }) => {
|
||||
test('Stock - Stock Actions', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
// Find an in-stock, untracked item
|
||||
await page.goto(
|
||||
`${baseUrl}/stock/location/index/stock-items?in_stock=1&serialized=0`
|
||||
);
|
||||
await page.getByText('530470210').first().click();
|
||||
await page.goto(`${baseUrl}/stock/item/1225/details`);
|
||||
|
||||
// Helper function to launch a stock action
|
||||
const launchStockAction = async (action: string) => {
|
||||
await page.getByLabel('action-menu-stock-operations').click();
|
||||
await page.getByLabel(`action-menu-stock-operations-${action}`).click();
|
||||
};
|
||||
|
||||
const setStockStatus = async (status: string) => {
|
||||
await page.getByLabel('action-button-change-status').click();
|
||||
await page.getByLabel('choice-field-status').click();
|
||||
await page.getByRole('option', { name: status }).click();
|
||||
};
|
||||
|
||||
// Check for required values
|
||||
await page.getByText('Status', { exact: true }).waitFor();
|
||||
await page.getByText('Custom Status', { exact: true }).waitFor();
|
||||
await page.getByText('Attention needed').waitFor();
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Quantity: 270$/ })
|
||||
.first()
|
||||
.getByLabel('Stock Details')
|
||||
.getByText('Incoming goods inspection')
|
||||
.waitFor();
|
||||
await page.getByText('123').first().waitFor();
|
||||
|
||||
// Check for expected action sections
|
||||
await page.getByLabel('action-menu-barcode-actions').click();
|
||||
await page.getByLabel('action-menu-barcode-actions-link-barcode').click();
|
||||
await page.getByRole('banner').getByRole('button').click();
|
||||
|
||||
await page.getByLabel('action-menu-printing-actions').click();
|
||||
await page.getByLabel('action-menu-printing-actions-print-labels').click();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
await page.getByLabel('action-menu-stock-operations').click();
|
||||
await page.getByLabel('action-menu-stock-operations-count').waitFor();
|
||||
await page.getByLabel('action-menu-stock-operations-add').waitFor();
|
||||
await page.getByLabel('action-menu-stock-operations-remove').waitFor();
|
||||
|
||||
await page.getByLabel('action-menu-stock-operations-transfer').click();
|
||||
await page.getByLabel('text-field-notes').fill('test notes');
|
||||
// Add stock, and change status
|
||||
await launchStockAction('add');
|
||||
await page.getByLabel('number-field-quantity').fill('12');
|
||||
await setStockStatus('Lost');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('This field is required.').first().waitFor();
|
||||
|
||||
// Set the status field
|
||||
await page.getByLabel('action-button-change-status').click();
|
||||
await page.getByLabel('choice-field-status').click();
|
||||
await page.getByText('Attention needed').click();
|
||||
await page.getByText('Lost').first().waitFor();
|
||||
await page.getByText('Unavailable').first().waitFor();
|
||||
await page.getByText('135').first().waitFor();
|
||||
|
||||
// Set the packaging field
|
||||
await page.getByLabel('action-button-adjust-packaging').click();
|
||||
await page.getByLabel('text-field-packaging').fill('test packaging');
|
||||
// Remove stock, and change status
|
||||
await launchStockAction('remove');
|
||||
await page.getByLabel('number-field-quantity').fill('99');
|
||||
await setStockStatus('Damaged');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Close the dialog
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
await page.getByText('36').first().waitFor();
|
||||
await page.getByText('Damaged').first().waitFor();
|
||||
|
||||
// Count stock and change status (reverting to original value)
|
||||
await launchStockAction('count');
|
||||
await page.getByLabel('number-field-quantity').fill('123');
|
||||
await setStockStatus('Incoming goods inspection');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByText('123').first().waitFor();
|
||||
await page.getByText('Custom Status').first().waitFor();
|
||||
await page.getByText('Incoming goods inspection').first().waitFor();
|
||||
|
||||
// Find an item which has been sent to a customer
|
||||
await page.goto(`${baseUrl}/stock/item/1014/details`);
|
||||
@ -220,7 +242,4 @@ test('Stock - Tracking', async ({ page }) => {
|
||||
await page.getByText('- - Factory/Office Block/Room').first().waitFor();
|
||||
await page.getByRole('link', { name: 'Widget Assembly' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'Installed into assembly' }).waitFor();
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
return;
|
||||
});
|
||||
|
@ -153,7 +153,7 @@ test('Settings - Admin - Barcode History', async ({ page, request }) => {
|
||||
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
||||
await page.getByRole('tab', { name: 'Barcode Scans' }).click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Barcode history is displayed in table
|
||||
barcodes.forEach(async (barcode) => {
|
||||
|
Reference in New Issue
Block a user