mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 10:18:42 +00:00
Merge branch 'master' into block-notes
This commit is contained in:
@@ -1191,6 +1191,10 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
for item in new_items:
|
for item in new_items:
|
||||||
item.set_status(status, custom_values=custom_stock_status_values)
|
item.set_status(status, custom_values=custom_stock_status_values)
|
||||||
|
# run validation for serialized items plugin.validate_batch_code
|
||||||
|
item.validate_batch_code()
|
||||||
|
# run validation for serialized items plugin.validate_model_instance
|
||||||
|
item.run_plugin_validation()
|
||||||
stock_items.append(item)
|
stock_items.append(item)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -1213,6 +1217,13 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
# Bulk create new stock items
|
# Bulk create new stock items
|
||||||
if len(bulk_create_items) > 0:
|
if len(bulk_create_items) > 0:
|
||||||
|
# bulk_create() bypasses save()/clean() methods, so manual validation is required for each item
|
||||||
|
for item in bulk_create_items:
|
||||||
|
# run validation for items plugin.validate_batch_code
|
||||||
|
item.validate_batch_code()
|
||||||
|
# run validation for items plugin.validate_model_instance
|
||||||
|
item.run_plugin_validation()
|
||||||
|
|
||||||
stock.models.StockItem.objects.bulk_create(
|
stock.models.StockItem.objects.bulk_create(
|
||||||
bulk_create_items, batch_size=250
|
bulk_create_items, batch_size=250
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,12 +47,14 @@ export default function OrderCalendar({
|
|||||||
role,
|
role,
|
||||||
params,
|
params,
|
||||||
filters,
|
filters,
|
||||||
|
initialFilters,
|
||||||
tooltip
|
tooltip
|
||||||
}: {
|
}: {
|
||||||
model: ModelType;
|
model: ModelType;
|
||||||
role: UserRoles;
|
role: UserRoles;
|
||||||
params: Record<string, any>;
|
params: Record<string, any>;
|
||||||
filters?: TableFilter[];
|
filters?: TableFilter[];
|
||||||
|
initialFilters?: TableFilter[];
|
||||||
tooltip?: (event: EventContentArg) => React.ReactNode;
|
tooltip?: (event: EventContentArg) => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -94,7 +96,8 @@ export default function OrderCalendar({
|
|||||||
const calendarState = useCalendar({
|
const calendarState = useCalendar({
|
||||||
endpoint: modelInfo.api_endpoint,
|
endpoint: modelInfo.api_endpoint,
|
||||||
name: model.toString(),
|
name: model.toString(),
|
||||||
queryParams: params
|
queryParams: params,
|
||||||
|
initialFilters: initialFilters
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build the events
|
// Build the events
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget';
|
|||||||
import GetStartedWidget from './widgets/GetStartedWidget';
|
import GetStartedWidget from './widgets/GetStartedWidget';
|
||||||
import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget';
|
import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget';
|
||||||
import NewsWidget from './widgets/NewsWidget';
|
import NewsWidget from './widgets/NewsWidget';
|
||||||
import OrderHistoryWidget from './widgets/OrderHistoryWidget';
|
|
||||||
import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget';
|
import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget';
|
||||||
import QueryDashboardWidget from './widgets/QueryDashboardWidget';
|
import QueryDashboardWidget from './widgets/QueryDashboardWidget';
|
||||||
import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget';
|
import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget';
|
||||||
@@ -218,26 +217,6 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function BuiltinHistoryWidgets(): DashboardWidgetProps[] {
|
|
||||||
return [
|
|
||||||
OrderHistoryWidget({
|
|
||||||
modelType: ModelType.build
|
|
||||||
}),
|
|
||||||
OrderHistoryWidget({
|
|
||||||
modelType: ModelType.salesorder
|
|
||||||
}),
|
|
||||||
OrderHistoryWidget({
|
|
||||||
modelType: ModelType.purchaseorder
|
|
||||||
}),
|
|
||||||
OrderHistoryWidget({
|
|
||||||
modelType: ModelType.returnorder
|
|
||||||
}),
|
|
||||||
OrderHistoryWidget({
|
|
||||||
modelType: ModelType.transferorder
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -276,7 +255,6 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] {
|
|||||||
export default function DashboardWidgetLibrary(): DashboardWidgetProps[] {
|
export default function DashboardWidgetLibrary(): DashboardWidgetProps[] {
|
||||||
return [
|
return [
|
||||||
...BuiltinQueryCountWidgets(),
|
...BuiltinQueryCountWidgets(),
|
||||||
...BuiltinHistoryWidgets(),
|
|
||||||
...BuiltinGettingStartedWidgets(),
|
...BuiltinGettingStartedWidgets(),
|
||||||
...BuiltinSettingsWidgets(),
|
...BuiltinSettingsWidgets(),
|
||||||
...BuiltinActionWidgets()
|
...BuiltinActionWidgets()
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
|
||||||
import type { ModelType } from '@lib/enums/ModelType';
|
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
|
||||||
import { StylishText } from '@lib/index';
|
|
||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { BarChart } from '@mantine/charts';
|
|
||||||
import { Box, LoadingOverlay, Stack } from '@mantine/core';
|
|
||||||
import { useDocumentVisibility } from '@mantine/hooks';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useApi } from '../../../contexts/ApiContext';
|
|
||||||
import { useUserState } from '../../../states/UserState';
|
|
||||||
import type { DashboardWidgetProps } from '../DashboardWidget';
|
|
||||||
|
|
||||||
function OrderHistoryComponent({
|
|
||||||
modelType,
|
|
||||||
title
|
|
||||||
}: {
|
|
||||||
modelType: ModelType;
|
|
||||||
title: string;
|
|
||||||
}) {
|
|
||||||
const modelInfo = useMemo(() => {
|
|
||||||
return ModelInformationDict[modelType];
|
|
||||||
}, [modelType]);
|
|
||||||
|
|
||||||
const url = useMemo(() => {
|
|
||||||
return apiUrl(modelInfo.api_endpoint);
|
|
||||||
}, [modelInfo]);
|
|
||||||
|
|
||||||
const api = useApi();
|
|
||||||
const visibility = useDocumentVisibility();
|
|
||||||
|
|
||||||
const endDate = dayjs().add(1, 'day').format('YYYY-MM-DD');
|
|
||||||
const startDate = dayjs()
|
|
||||||
.subtract(12, 'month')
|
|
||||||
.subtract(1, 'day')
|
|
||||||
.format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
completed_after: startDate,
|
|
||||||
completed_before: endDate
|
|
||||||
};
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: ['dashboard-order-summary', modelType, params, visibility],
|
|
||||||
enabled: visibility === 'visible',
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
refetchOnMount: true,
|
|
||||||
refetchInterval: 10 * 60 * 1000, // 10 minute refetch interval
|
|
||||||
staleTime: 5 * 60 * 1000, // 5 minute stale time
|
|
||||||
queryFn: () => {
|
|
||||||
if (visibility !== 'visible') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.get(url, { params }).then((res) => {
|
|
||||||
return res.data ?? [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
|
||||||
const months = Array.from({ length: 13 }, (_, i) => ({
|
|
||||||
month: dayjs()
|
|
||||||
.subtract(12 - i, 'month')
|
|
||||||
.format('MMM YY'),
|
|
||||||
count: 0
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const order of query.data || []) {
|
|
||||||
// Build orders use `completion_date`; all other order types use `complete_date`
|
|
||||||
const dateStr =
|
|
||||||
order.complete_date ?? order.completion_date ?? order.shipment_date;
|
|
||||||
if (!dateStr) continue;
|
|
||||||
const label = dayjs(dateStr).format('MMM YY');
|
|
||||||
const entry = months.find((m) => m.month === label);
|
|
||||||
if (entry) entry.count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return months;
|
|
||||||
}, [query.data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap='xs'>
|
|
||||||
<StylishText size='md'>{title}</StylishText>
|
|
||||||
<Box>
|
|
||||||
<LoadingOverlay visible={query.isLoading || query.isFetching} />
|
|
||||||
<BarChart
|
|
||||||
h={200}
|
|
||||||
data={chartData}
|
|
||||||
dataKey='month'
|
|
||||||
series={[{ name: 'count', label: t`Completed`, color: 'blue.6' }]}
|
|
||||||
withYAxis={false}
|
|
||||||
yAxisProps={{ domain: [0, 'auto'] }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a simple chart of the number of completed orders per month, for the last 12 months.
|
|
||||||
*/
|
|
||||||
export default function OrderHistoryWidget({
|
|
||||||
modelType
|
|
||||||
}: {
|
|
||||||
modelType: ModelType;
|
|
||||||
}): DashboardWidgetProps {
|
|
||||||
const user = useUserState();
|
|
||||||
|
|
||||||
const modelInfo = useMemo(() => {
|
|
||||||
return ModelInformationDict[modelType];
|
|
||||||
}, [modelType]);
|
|
||||||
|
|
||||||
// Extract translated model labels
|
|
||||||
const models = modelInfo.label_multiple();
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: `${modelType}-history`,
|
|
||||||
title: t`Completed ${models}`,
|
|
||||||
description: t`Display number of completed ${models} per month`,
|
|
||||||
minHeight: 2,
|
|
||||||
minWidth: 3,
|
|
||||||
modelType: modelType,
|
|
||||||
icon: 'chart_bar',
|
|
||||||
visible: () => user.hasViewPermission(modelType),
|
|
||||||
render: () => (
|
|
||||||
<OrderHistoryComponent
|
|
||||||
modelType={modelType}
|
|
||||||
title={t`Completed ${models}`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import type FullCalendar from '@fullcalendar/react';
|
|||||||
import type { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import type { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import useFilterSet from '@lib/hooks/UseFilterSet';
|
import useFilterSet from '@lib/hooks/UseFilterSet';
|
||||||
import type { FilterSetState } from '@lib/types/Filters';
|
import type { FilterSetState, TableFilter } from '@lib/types/Filters';
|
||||||
import type { UseModalReturn } from '@lib/types/Modals';
|
import type { UseModalReturn } from '@lib/types/Modals';
|
||||||
import type { DateValue } from '@mantine/dates';
|
import type { DateValue } from '@mantine/dates';
|
||||||
import { type UseQueryResult, useQuery } from '@tanstack/react-query';
|
import { type UseQueryResult, useQuery } from '@tanstack/react-query';
|
||||||
@@ -54,15 +54,17 @@ export type CalendarState = {
|
|||||||
export default function useCalendar({
|
export default function useCalendar({
|
||||||
name,
|
name,
|
||||||
endpoint,
|
endpoint,
|
||||||
queryParams
|
queryParams,
|
||||||
|
initialFilters
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
endpoint: ApiEndpoints;
|
endpoint: ApiEndpoints;
|
||||||
queryParams?: any;
|
queryParams?: any;
|
||||||
|
initialFilters?: TableFilter[];
|
||||||
}): CalendarState {
|
}): CalendarState {
|
||||||
const ref = useRef<FullCalendar>(null as any);
|
const ref = useRef<FullCalendar>(null as any);
|
||||||
|
|
||||||
const filterSet = useFilterSet(`calendar-${name}`);
|
const filterSet = useFilterSet(`calendar-${name}`, initialFilters);
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ function BuildOrderCalendar() {
|
|||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.build}
|
model={ModelType.build}
|
||||||
role={UserRoles.build}
|
role={UserRoles.build}
|
||||||
params={{ outstanding: true, part_detail: true }}
|
params={{ part_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||||
tooltip={renderTooltip}
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ function PurchaseOrderCalendar() {
|
|||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.purchaseorder}
|
model={ModelType.purchaseorder}
|
||||||
role={UserRoles.purchase_order}
|
role={UserRoles.purchase_order}
|
||||||
params={{ outstanding: true, supplier_detail: true }}
|
params={{ supplier_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||||
tooltip={renderTooltip}
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ function SalesOrderCalendar() {
|
|||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.salesorder}
|
model={ModelType.salesorder}
|
||||||
role={UserRoles.sales_order}
|
role={UserRoles.sales_order}
|
||||||
params={{ outstanding: true, customer_detail: true }}
|
params={{ customer_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||||
tooltip={renderTooltip}
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -73,8 +74,9 @@ const ReturnOrderCalendar = () => {
|
|||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.returnorder}
|
model={ModelType.returnorder}
|
||||||
role={UserRoles.return_order}
|
role={UserRoles.return_order}
|
||||||
params={{ outstanding: true, customer_detail: true }}
|
params={{ customer_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||||
tooltip={renderTooltip}
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,8 +70,9 @@ function TransferOrderCalendar() {
|
|||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.transferorder}
|
model={ModelType.transferorder}
|
||||||
role={UserRoles.transfer_order}
|
role={UserRoles.transfer_order}
|
||||||
params={{ outstanding: true }}
|
params={{}}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
import { ActionButton } from '@lib/components/ActionButton';
|
||||||
|
import { ProgressBar } from '@lib/components/ProgressBar';
|
||||||
|
import { RowEditAction, RowViewAction } from '@lib/components/RowActions';
|
||||||
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import { formatDecimal } from '@lib/functions/Formatting';
|
||||||
|
import useTable from '@lib/hooks/UseTable';
|
||||||
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
|
import type { RowAction, TableColumn } from '@lib/types/Tables';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Alert, Group, Paper, Text } from '@mantine/core';
|
import { Alert, Group, Paper, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
@@ -13,18 +24,6 @@ import {
|
|||||||
import type { DataTableRowExpansionProps } from 'mantine-datatable';
|
import type { DataTableRowExpansionProps } from 'mantine-datatable';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ActionButton } from '@lib/components/ActionButton';
|
|
||||||
import { ProgressBar } from '@lib/components/ProgressBar';
|
|
||||||
import { RowEditAction, RowViewAction } from '@lib/components/RowActions';
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
|
||||||
import { formatDecimal } from '@lib/functions/Formatting';
|
|
||||||
import useTable from '@lib/hooks/UseTable';
|
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
|
||||||
import type { RowAction, TableColumn } from '@lib/types/Tables';
|
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import {
|
import {
|
||||||
useAllocateStockToBuildForm,
|
useAllocateStockToBuildForm,
|
||||||
@@ -769,7 +768,9 @@ export default function BuildLineTable({
|
|||||||
!consumable &&
|
!consumable &&
|
||||||
user.hasChangeRole(UserRoles.build) &&
|
user.hasChangeRole(UserRoles.build) &&
|
||||||
required > 0 &&
|
required > 0 &&
|
||||||
record.trackable == hasOutput;
|
(hasOutput ? trackable : true);
|
||||||
|
|
||||||
|
const disableAllocation = !hasOutput && trackable;
|
||||||
|
|
||||||
// Can de-allocate
|
// Can de-allocate
|
||||||
const canDeallocate =
|
const canDeallocate =
|
||||||
@@ -792,6 +793,10 @@ export default function BuildLineTable({
|
|||||||
icon: <IconArrowRight />,
|
icon: <IconArrowRight />,
|
||||||
title: t`Allocate Stock`,
|
title: t`Allocate Stock`,
|
||||||
hidden: !canAllocate,
|
hidden: !canAllocate,
|
||||||
|
disabled: disableAllocation,
|
||||||
|
tooltip: disableAllocation
|
||||||
|
? t`Trackable parts must be allocated via the Build Outputs tab`
|
||||||
|
: t`Allocate Stock`,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedRows([record]);
|
setSelectedRows([record]);
|
||||||
|
|||||||
@@ -418,6 +418,18 @@ test('Build Order - Allocation', async ({ browser }) => {
|
|||||||
|
|
||||||
await row.getByText(/150 \/ 150/).waitFor();
|
await row.getByText(/150 \/ 150/).waitFor();
|
||||||
|
|
||||||
|
// Open the allocation menu for the red widget
|
||||||
|
const mainRedWidget = await page.getByRole('cell', { name: 'Red Widget' });
|
||||||
|
const mainRedRow = await getRowFromCell(mainRedWidget);
|
||||||
|
|
||||||
|
await mainRedRow.getByLabel(/row-action-menu-/i).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'Allocate Stock', exact: true })
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
// Expand this row
|
// Expand this row
|
||||||
await cell.click();
|
await cell.click();
|
||||||
await page.getByRole('cell', { name: '2022-4-27', exact: true }).waitFor();
|
await page.getByRole('cell', { name: '2022-4-27', exact: true }).waitFor();
|
||||||
@@ -494,9 +506,13 @@ test('Build Order - Allocation', async ({ browser }) => {
|
|||||||
const redRow = await getRowFromCell(redWidget);
|
const redRow = await getRowFromCell(redWidget);
|
||||||
|
|
||||||
await redRow.getByLabel(/row-action-menu-/i).click();
|
await redRow.getByLabel(/row-action-menu-/i).click();
|
||||||
await page
|
|
||||||
.getByRole('menuitem', { name: 'Allocate Stock', exact: true })
|
const allocateStockBtn = page.getByRole('menuitem', {
|
||||||
.waitFor();
|
name: 'Allocate Stock',
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
await expect(allocateStockBtn).toBeEnabled();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('menuitem', { name: 'Deallocate Stock', exact: true })
|
.getByRole('menuitem', { name: 'Deallocate Stock', exact: true })
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|||||||
Reference in New Issue
Block a user