mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Merge branch 'master' into matmair/issue6281
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Box, Divider, Modal } from '@mantine/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import type { ModelType } from '../../enums/ModelType';
|
||||
@ -17,17 +17,34 @@ export default function BarcodeScanDialog({
|
||||
title,
|
||||
opened,
|
||||
onClose
|
||||
}: {
|
||||
}: Readonly<{
|
||||
title?: string;
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
}>) {
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size='lg'
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<StylishText size='xl'>{title ?? t`Scan Barcode`}</StylishText>}
|
||||
>
|
||||
<Divider />
|
||||
<Box>
|
||||
<ScanInputHandler navigate={navigate} onClose={onClose} />
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
export function ScanInputHandler({
|
||||
onClose,
|
||||
navigate
|
||||
}: Readonly<{ onClose: () => void; navigate: NavigateFunction }>) {
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const user = useUserState();
|
||||
|
||||
const onScan = useCallback((barcode: string) => {
|
||||
if (!barcode || barcode.length === 0) {
|
||||
@ -80,19 +97,5 @@ export default function BarcodeScanDialog({
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
size='lg'
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<StylishText size='xl'>{title ?? t`Scan Barcode`}</StylishText>}
|
||||
>
|
||||
<Divider />
|
||||
<Box>
|
||||
<BarcodeInput onScan={onScan} error={error} processing={processing} />
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
return <BarcodeInput onScan={onScan} error={error} processing={processing} />;
|
||||
}
|
||||
|
21
src/frontend/src/components/modals/QrModal.tsx
Normal file
21
src/frontend/src/components/modals/QrModal.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import {} from '@mantine/core';
|
||||
import type { ContextModalProps } from '@mantine/modals';
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
import { ScanInputHandler } from '../barcodes/BarcodeScanDialog';
|
||||
|
||||
export function QrModal({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: Readonly<
|
||||
ContextModalProps<{ modalBody: string; navigate: NavigateFunction }>
|
||||
>) {
|
||||
function close() {
|
||||
context.closeModal(id);
|
||||
}
|
||||
function navigate() {
|
||||
context.closeModal(id);
|
||||
}
|
||||
|
||||
return <ScanInputHandler navigate={innerProps.navigate} onClose={close} />;
|
||||
}
|
@ -1,17 +1,24 @@
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
import type { ModelType } from '../../enums/ModelType';
|
||||
import type { UserRoles } from '../../enums/Roles';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import ClientError from '../errors/ClientError';
|
||||
import PermissionDenied from '../errors/PermissionDenied';
|
||||
import ServerError from '../errors/ServerError';
|
||||
|
||||
export default function InstanceDetail({
|
||||
status,
|
||||
loading,
|
||||
children
|
||||
children,
|
||||
requiredRole,
|
||||
requiredPermission
|
||||
}: Readonly<{
|
||||
status: number;
|
||||
loading: boolean;
|
||||
children: React.ReactNode;
|
||||
requiredRole?: UserRoles;
|
||||
requiredPermission?: ModelType;
|
||||
}>) {
|
||||
const user = useUserState();
|
||||
|
||||
@ -27,5 +34,13 @@ export default function InstanceDetail({
|
||||
return <ClientError status={status} />;
|
||||
}
|
||||
|
||||
if (requiredRole && !user.hasViewRole(requiredRole)) {
|
||||
return <PermissionDenied />;
|
||||
}
|
||||
|
||||
if (requiredPermission && !user.hasViewPermission(requiredPermission)) {
|
||||
return <PermissionDenied />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -6,6 +6,7 @@ import { ContextMenuProvider } from 'mantine-contextmenu';
|
||||
|
||||
import { AboutInvenTreeModal } from '../components/modals/AboutInvenTreeModal';
|
||||
import { LicenseModal } from '../components/modals/LicenseModal';
|
||||
import { QrModal } from '../components/modals/QrModal';
|
||||
import { ServerInfoModal } from '../components/modals/ServerInfoModal';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { LanguageContext } from './LanguageContext';
|
||||
@ -47,7 +48,8 @@ export function ThemeContext({
|
||||
modals={{
|
||||
info: ServerInfoModal,
|
||||
about: AboutInvenTreeModal,
|
||||
license: LicenseModal
|
||||
license: LicenseModal,
|
||||
qr: QrModal
|
||||
}}
|
||||
>
|
||||
<Notifications />
|
||||
|
@ -1,12 +1,20 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import type { SpotlightActionData } from '@mantine/spotlight';
|
||||
import { IconLink, IconPointer } from '@tabler/icons-react';
|
||||
import { IconBarcode, IconLink, IconPointer } from '@tabler/icons-react';
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
import { openContextModal } from '@mantine/modals';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { useUserState } from '../states/UserState';
|
||||
import { aboutInvenTree, docLinks, licenseInfo, serverInfo } from './links';
|
||||
|
||||
export function openQrModal(navigate: NavigateFunction) {
|
||||
return openContextModal({
|
||||
modal: 'qr',
|
||||
innerProps: { navigate: navigate }
|
||||
});
|
||||
}
|
||||
|
||||
export function getActions(navigate: NavigateFunction) {
|
||||
const setNavigationOpen = useLocalState((state) => state.setNavigationOpen);
|
||||
const { user } = useUserState();
|
||||
@ -55,6 +63,13 @@ export function getActions(navigate: NavigateFunction) {
|
||||
description: t`Open the main navigation menu`,
|
||||
onClick: () => setNavigationOpen(true),
|
||||
leftSection: <IconPointer size='1.2rem' />
|
||||
},
|
||||
{
|
||||
id: 'scan',
|
||||
label: t`Scan`,
|
||||
description: t`Scan a barcode or QR code`,
|
||||
onClick: () => openQrModal(navigate),
|
||||
leftSection: <IconBarcode size='1.2rem' />
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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]);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
@ -510,7 +519,11 @@ export default function BuildDetail() {
|
||||
{holdOrder.modal}
|
||||
{issueOrder.modal}
|
||||
{completeOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
requiredRole={UserRoles.build}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
title={`${t`Build Order`}: ${build.reference}`}
|
||||
|
@ -398,7 +398,11 @@ export default function SupplierPartDetail() {
|
||||
{deleteSupplierPart.modal}
|
||||
{duplicateSupplierPart.modal}
|
||||
{editSupplierPart.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
requiredRole={UserRoles.purchase_order}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
title={t`Supplier Part`}
|
||||
|
@ -318,6 +318,7 @@ export default function CategoryDetail() {
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={id ? instanceQuery.isFetching : false}
|
||||
requiredRole={UserRoles.part_category}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
|
@ -970,18 +970,24 @@ export default function PartDetail() {
|
||||
{editPart.modal}
|
||||
{deletePart.modal}
|
||||
{orderPartsWizard.wizard}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
requiredRole={UserRoles.part}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<NavigationTree
|
||||
title={t`Part Categories`}
|
||||
modelType={ModelType.partcategory}
|
||||
endpoint={ApiEndpoints.category_tree}
|
||||
opened={treeOpen}
|
||||
onClose={() => {
|
||||
setTreeOpen(false);
|
||||
}}
|
||||
selectedId={part?.category}
|
||||
/>
|
||||
{user.hasViewRole(UserRoles.part_category) && (
|
||||
<NavigationTree
|
||||
title={t`Part Categories`}
|
||||
modelType={ModelType.partcategory}
|
||||
endpoint={ApiEndpoints.category_tree}
|
||||
opened={treeOpen}
|
||||
onClose={() => {
|
||||
setTreeOpen(false);
|
||||
}}
|
||||
selectedId={part?.category}
|
||||
/>
|
||||
)}
|
||||
<PageDetail
|
||||
title={`${t`Part`}: ${part.full_name}`}
|
||||
icon={
|
||||
@ -992,9 +998,13 @@ export default function PartDetail() {
|
||||
subtitle={part.description}
|
||||
imageUrl={part.image}
|
||||
badges={badges}
|
||||
breadcrumbs={breadcrumbs}
|
||||
breadcrumbs={
|
||||
user.hasViewRole(UserRoles.part_category)
|
||||
? breadcrumbs
|
||||
: undefined
|
||||
}
|
||||
breadcrumbAction={() => {
|
||||
setTreeOpen(true); // Open the category tree
|
||||
setTreeOpen(true);
|
||||
}}
|
||||
editAction={editPart.open}
|
||||
editEnabled={user.hasChangeRole(UserRoles.part)}
|
||||
|
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
@ -474,7 +483,11 @@ export default function PurchaseOrderDetail() {
|
||||
{completeOrder.modal}
|
||||
{editPurchaseOrder.modal}
|
||||
{duplicatePurchaseOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
requiredRole={UserRoles.purchase_order}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
title={`${t`Purchase Order`}: ${order.reference}`}
|
||||
|
@ -24,7 +24,8 @@ export default function PurchasingIndex() {
|
||||
name: 'purchaseorders',
|
||||
label: t`Purchase Orders`,
|
||||
icon: <IconShoppingCart />,
|
||||
content: <PurchaseOrderTable />
|
||||
content: <PurchaseOrderTable />,
|
||||
hidden: !user.hasViewRole(UserRoles.purchase_order)
|
||||
},
|
||||
{
|
||||
name: 'suppliers',
|
||||
@ -49,7 +50,7 @@ export default function PurchasingIndex() {
|
||||
)
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.purchase_order)) {
|
||||
return <PermissionDenied />;
|
||||
|
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
@ -461,7 +470,11 @@ export default function ReturnOrderDetail() {
|
||||
{holdOrder.modal}
|
||||
{completeOrder.modal}
|
||||
{duplicateReturnOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
requiredRole={UserRoles.return_order}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
title={`${t`Return Order`}: ${order.reference}`}
|
||||
|
@ -25,13 +25,15 @@ export default function PurchasingIndex() {
|
||||
name: 'salesorders',
|
||||
label: t`Sales Orders`,
|
||||
icon: <IconTruckDelivery />,
|
||||
content: <SalesOrderTable />
|
||||
content: <SalesOrderTable />,
|
||||
hidden: !user.hasViewRole(UserRoles.sales_order)
|
||||
},
|
||||
{
|
||||
name: 'returnorders',
|
||||
label: t`Return Orders`,
|
||||
icon: <IconTruckReturn />,
|
||||
content: <ReturnOrderTable />
|
||||
content: <ReturnOrderTable />,
|
||||
hidden: !user.hasViewRole(UserRoles.return_order)
|
||||
},
|
||||
{
|
||||
name: 'suppliers',
|
||||
@ -42,7 +44,7 @@ export default function PurchasingIndex() {
|
||||
)
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.sales_order)) {
|
||||
return <PermissionDenied />;
|
||||
|
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
@ -525,7 +534,11 @@ export default function SalesOrderDetail() {
|
||||
{completeOrder.modal}
|
||||
{editSalesOrder.modal}
|
||||
{duplicateSalesOrder.modal}
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
requiredRole={UserRoles.sales_order}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
title={`${t`Sales Order`}: ${order.reference}`}
|
||||
|
@ -352,6 +352,7 @@ export default function SalesOrderShipmentDetail() {
|
||||
<InstanceDetail
|
||||
status={shipmentStatus}
|
||||
loading={shipmentQuery.isFetching || customerQuery.isFetching}
|
||||
requiredRole={UserRoles.sales_order}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
|
@ -280,6 +280,8 @@ export default function Stock() {
|
||||
<BarcodeActionDropdown
|
||||
model={ModelType.stocklocation}
|
||||
pk={location.pk}
|
||||
hash={location?.barcode_hash}
|
||||
perm={user.hasChangeRole(UserRoles.stock_location)}
|
||||
actions={[
|
||||
{
|
||||
name: 'Scan in stock items',
|
||||
@ -363,6 +365,7 @@ export default function Stock() {
|
||||
<InstanceDetail
|
||||
status={requestStatus}
|
||||
loading={id ? instanceQuery.isFetching : false}
|
||||
requiredRole={UserRoles.stock_location}
|
||||
>
|
||||
<Stack>
|
||||
<NavigationTree
|
||||
|
@ -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'
|
||||
/>,
|
||||
@ -875,16 +884,22 @@ export default function StockDetail() {
|
||||
}, [stockitem, instanceQuery, enableExpiry]);
|
||||
|
||||
return (
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<InstanceDetail
|
||||
requiredRole={UserRoles.stock}
|
||||
status={requestStatus}
|
||||
loading={instanceQuery.isFetching}
|
||||
>
|
||||
<Stack>
|
||||
<NavigationTree
|
||||
title={t`Stock Locations`}
|
||||
modelType={ModelType.stocklocation}
|
||||
endpoint={ApiEndpoints.stock_location_tree}
|
||||
opened={treeOpen}
|
||||
onClose={() => setTreeOpen(false)}
|
||||
selectedId={stockitem?.location}
|
||||
/>
|
||||
{user.hasViewRole(UserRoles.stock_location) && (
|
||||
<NavigationTree
|
||||
title={t`Stock Locations`}
|
||||
modelType={ModelType.stocklocation}
|
||||
endpoint={ApiEndpoints.stock_location_tree}
|
||||
opened={treeOpen}
|
||||
onClose={() => setTreeOpen(false)}
|
||||
selectedId={stockitem?.location}
|
||||
/>
|
||||
)}
|
||||
<PageDetail
|
||||
title={t`Stock Item`}
|
||||
subtitle={stockitem.part_detail?.full_name}
|
||||
@ -892,7 +907,9 @@ export default function StockDetail() {
|
||||
editAction={editStockItem.open}
|
||||
editEnabled={user.hasChangePermission(ModelType.stockitem)}
|
||||
badges={stockBadges}
|
||||
breadcrumbs={breadcrumbs}
|
||||
breadcrumbs={
|
||||
user.hasViewRole(UserRoles.stock_location) ? breadcrumbs : undefined
|
||||
}
|
||||
breadcrumbAction={() => {
|
||||
setTreeOpen(true);
|
||||
}}
|
||||
|
@ -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