diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 8072d2e14a56..d54290fcdc81 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,17 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 278 +INVENTREE_API_VERSION = 279 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v279 - 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458 + - Adds "order_outstanding" and "part" filters to the BuildLine API endpoint + - Adds "order_outstanding" filter to the SalesOrderLineItem API endpoint + v278 - 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445 - Updates to the SalesOrder API endpoints - Add "shipment count" information to the SalesOrder API endpoints diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index d65d8980b968..096a42683e65 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -357,6 +357,23 @@ class Meta: tracked = rest_filters.BooleanFilter(label=_('Tracked'), field_name='bom_item__sub_part__trackable') testable = rest_filters.BooleanFilter(label=_('Testable'), field_name='bom_item__sub_part__testable') + part = rest_filters.ModelChoiceFilter( + queryset=part.models.Part.objects.all(), + label=_('Part'), + field_name='bom_item__sub_part', + ) + + order_outstanding = rest_filters.BooleanFilter( + label=_('Order Outstanding'), + method='filter_order_outstanding' + ) + + def filter_order_outstanding(self, queryset, name, value): + """Filter by whether the associated BuildOrder is 'outstanding'.""" + if str2bool(value): + return queryset.filter(build__status__in=BuildStatusGroups.ACTIVE_CODES) + return queryset.exclude(build__status__in=BuildStatusGroups.ACTIVE_CODES) + allocated = rest_filters.BooleanFilter(label=_('Allocated'), method='filter_allocated') def filter_allocated(self, queryset, name, value): @@ -383,12 +400,28 @@ def filter_available(self, queryset, name, value): return queryset.exclude(flt) + class BuildLineEndpoint: """Mixin class for BuildLine API endpoints.""" queryset = BuildLine.objects.all() serializer_class = build.serializers.BuildLineSerializer + def get_serializer(self, *args, **kwargs): + """Return the serializer instance for this endpoint.""" + + kwargs['context'] = self.get_serializer_context() + + try: + params = self.request.query_params + + kwargs['part_detail'] = str2bool(params.get('part_detail', True)) + kwargs['build_detail'] = str2bool(params.get('build_detail', False)) + except AttributeError: + pass + + return self.serializer_class(*args, **kwargs) + def get_source_build(self) -> Build: """Return the source Build object for the BuildLine queryset. diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 236641b69ccd..845cd584bd32 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -1278,8 +1278,6 @@ class Meta: 'pk', 'build', 'bom_item', - 'bom_item_detail', - 'part_detail', 'quantity', # Build detail fields @@ -1315,6 +1313,11 @@ class Meta: # Extra fields only for data export 'part_description', 'part_category_name', + + # Extra detail (related field) serializers + 'bom_item_detail', + 'part_detail', + 'build_detail', ] read_only_fields = [ @@ -1323,6 +1326,19 @@ class Meta: 'allocations', ] + def __init__(self, *args, **kwargs): + """Determine which extra details fields should be included""" + part_detail = kwargs.pop('part_detail', True) + build_detail = kwargs.pop('build_detail', False) + + super().__init__(*args, **kwargs) + + if not part_detail: + self.fields.pop('part_detail', None) + + if not build_detail: + self.fields.pop('build_detail', None) + # Build info fields build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True) @@ -1362,6 +1378,7 @@ class Meta: ) part_detail = part_serializers.PartBriefSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False) + build_detail = BuildSerializer(source='build', part_detail=False, many=False, read_only=True) # Annotated (calculated) fields @@ -1404,9 +1421,13 @@ def annotate_queryset(queryset, build=None): """ queryset = queryset.select_related( 'build', + 'build__part', + 'build__part__pricing_data', 'bom_item', 'bom_item__part', + 'bom_item__part__pricing_data', 'bom_item__sub_part', + 'bom_item__sub_part__pricing_data' ) # Pre-fetch related fields diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index d7e4807208ce..b5671c59525b 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -816,6 +816,17 @@ def filter_order_complete(self, queryset, name, value): return queryset.exclude(order__status__in=SalesOrderStatusGroups.COMPLETE) + order_outstanding = rest_filters.BooleanFilter( + label=_('Order Outstanding'), method='filter_order_outstanding' + ) + + def filter_order_outstanding(self, queryset, name, value): + """Filter by whether the order is 'outstanding' or not.""" + if str2bool(value): + return queryset.filter(order__status__in=SalesOrderStatusGroups.OPEN) + + return queryset.exclude(order__status__in=SalesOrderStatusGroups.OPEN) + class SalesOrderLineItemMixin: """Mixin class for SalesOrderLineItem endpoints.""" diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index 60a24d6290fe..f0c52403c307 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -497,9 +497,9 @@ export function useAllocateStockToBuildForm({ lineItems, onFormSuccess }: { - buildId: number; + buildId?: number; outputId?: number | null; - build: any; + build?: any; lineItems: any[]; onFormSuccess: (response: any) => void; }) { @@ -533,8 +533,8 @@ export function useAllocateStockToBuildForm({ }, [lineItems, sourceLocation]); useEffect(() => { - setSourceLocation(build.take_from); - }, [build.take_from]); + setSourceLocation(build?.take_from); + }, [build?.take_from]); const sourceLocationField: ApiFormFieldType = useMemo(() => { return { @@ -545,7 +545,7 @@ export function useAllocateStockToBuildForm({ label: t`Source Location`, description: t`Select the source location for the stock allocation`, name: 'source_location', - value: build.take_from, + value: build?.take_from, onValueChange: (value: any) => { setSourceLocation(value); } diff --git a/src/frontend/src/pages/part/PartAllocationPanel.tsx b/src/frontend/src/pages/part/PartAllocationPanel.tsx index e64a8a999ab0..7fe25a9d93d6 100644 --- a/src/frontend/src/pages/part/PartAllocationPanel.tsx +++ b/src/frontend/src/pages/part/PartAllocationPanel.tsx @@ -2,11 +2,10 @@ import { t } from '@lingui/macro'; import { Accordion } from '@mantine/core'; import { StylishText } from '../../components/items/StylishText'; -import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useUserState } from '../../states/UserState'; -import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable'; -import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable'; +import PartBuildAllocationsTable from '../../tables/part/PartBuildAllocationsTable'; +import PartSalesAllocationsTable from '../../tables/part/PartSalesAllocationsTable'; export default function PartAllocationPanel({ part }: { part: any }) { const user = useUserState(); @@ -23,14 +22,7 @@ export default function PartAllocationPanel({ part }: { part: any }) { {t`Build Order Allocations`} - + )} @@ -40,12 +32,7 @@ export default function PartAllocationPanel({ part }: { part: any }) { {t`Sales Order Allocations`} - + )} diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index 371cc58eb4ab..8a6d3667b6f5 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -163,10 +163,16 @@ export function LineItemsProgressColumn(): TableColumn { export function ProjectCodeColumn(props: TableColumnProps): TableColumn { return { accessor: 'project_code', + ordering: 'project_code', sortable: true, - render: (record: any) => ( - - ), + title: t`Project Code`, + render: (record: any) => { + let project_code = resolveItem( + record, + props.accessor ?? 'project_code_detail' + ); + return ; + }, ...props }; } diff --git a/src/frontend/src/tables/RowExpansionIcon.tsx b/src/frontend/src/tables/RowExpansionIcon.tsx new file mode 100644 index 000000000000..3313778752c1 --- /dev/null +++ b/src/frontend/src/tables/RowExpansionIcon.tsx @@ -0,0 +1,16 @@ +import { ActionIcon } from '@mantine/core'; +import { IconChevronDown, IconChevronRight } from '@tabler/icons-react'; + +export default function RowExpansionIcon({ + enabled, + expanded +}: { + enabled: boolean; + expanded: boolean; +}) { + return ( + + {expanded ? : } + + ); +} diff --git a/src/frontend/src/tables/bom/UsedInTable.tsx b/src/frontend/src/tables/bom/UsedInTable.tsx index 84c09ad38f80..577942160281 100644 --- a/src/frontend/src/tables/bom/UsedInTable.tsx +++ b/src/frontend/src/tables/bom/UsedInTable.tsx @@ -51,12 +51,13 @@ export function UsedInTable({ }, { accessor: 'quantity', + switchable: false, render: (record: any) => { let quantity = formatDecimal(record.quantity); let units = record.sub_part_detail?.units; return ( - + {quantity} {units && {units}} diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index ecef78cb8cd6..97ded0a9df84 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -1,9 +1,7 @@ import { t } from '@lingui/macro'; -import { ActionIcon, Alert, Group, Paper, Stack, Text } from '@mantine/core'; +import { Alert, Group, Paper, Stack, Text } from '@mantine/core'; import { IconArrowRight, - IconChevronDown, - IconChevronRight, IconCircleMinus, IconShoppingCart, IconTool, @@ -43,6 +41,7 @@ import { RowEditAction, RowViewAction } from '../RowActions'; +import RowExpansionIcon from '../RowExpansionIcon'; import { TableHoverCard } from '../TableHoverCard'; /** @@ -53,16 +52,17 @@ import { TableHoverCard } from '../TableHoverCard'; * * Note: We expect that the "lineItem" object contains an allocations[] list */ -function BuildLineSubTable({ +export function BuildLineSubTable({ lineItem, onEditAllocation, onDeleteAllocation }: { lineItem: any; - onEditAllocation: (pk: number) => void; - onDeleteAllocation: (pk: number) => void; + onEditAllocation?: (pk: number) => void; + onDeleteAllocation?: (pk: number) => void; }) { const user = useUserState(); + const navigate = useNavigate(); const tableColumns: any[] = useMemo(() => { return [ @@ -100,16 +100,24 @@ function BuildLineSubTable({ title={t`Actions`} index={record.pk} actions={[ + RowViewAction({ + title: t`View Stock Item`, + modelType: ModelType.stockitem, + modelId: record.stock_item, + navigate: navigate + }), RowEditAction({ - hidden: !user.hasChangeRole(UserRoles.build), + hidden: + !onEditAllocation || !user.hasChangeRole(UserRoles.build), onClick: () => { - onEditAllocation(record.pk); + onEditAllocation?.(record.pk); } }), RowDeleteAction({ - hidden: !user.hasDeleteRole(UserRoles.build), + hidden: + !onDeleteAllocation || !user.hasDeleteRole(UserRoles.build), onClick: () => { - onDeleteAllocation(record.pk); + onDeleteAllocation?.(record.pk); } }) ]} @@ -131,7 +139,7 @@ function BuildLineSubTable({ pinLastColumn idAccessor="pk" columns={tableColumns} - records={lineItem.filteredAllocations} + records={lineItem.filteredAllocations ?? lineItem.allocations} /> @@ -301,17 +309,10 @@ export default function BuildLineTable({ return ( - - {table.isRowExpanded(record.pk) ? ( - - ) : ( - - )} - + ); diff --git a/src/frontend/src/tables/part/PartBuildAllocationsTable.tsx b/src/frontend/src/tables/part/PartBuildAllocationsTable.tsx new file mode 100644 index 000000000000..b3dd4be29c10 --- /dev/null +++ b/src/frontend/src/tables/part/PartBuildAllocationsTable.tsx @@ -0,0 +1,130 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { DataTableRowExpansionProps } from 'mantine-datatable'; +import { useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { ProgressBar } from '../../components/items/ProgressBar'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { + DescriptionColumn, + ProjectCodeColumn, + StatusColumn +} from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowViewAction } from '../RowActions'; +import RowExpansionIcon from '../RowExpansionIcon'; +import { BuildLineSubTable } from '../build/BuildLineTable'; + +/** + * A "simplified" BuildOrderLineItem table showing all outstanding build order allocations for a given part. + */ +export default function PartBuildAllocationsTable({ + partId +}: { + partId: number; +}) { + const user = useUserState(); + const navigate = useNavigate(); + const table = useTable('part-build-allocations'); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'build', + title: t`Build Order`, + sortable: true, + render: (record: any) => ( + + 0} + expanded={table.isRowExpanded(record.pk)} + /> + {record.build_detail?.reference} + + ) + }, + DescriptionColumn({ + accessor: 'build_detail.title' + }), + ProjectCodeColumn({ + accessor: 'build_detail.project_code_detail' + }), + StatusColumn({ + accessor: 'build_detail.status', + model: ModelType.build, + title: t`Order Status` + }), + { + accessor: 'allocated', + sortable: true, + title: t`Required Stock`, + render: (record: any) => ( + + ) + } + ]; + }, [table.isRowExpanded]); + + const rowActions = useCallback( + (record: any) => { + return [ + RowViewAction({ + title: t`View Build Order`, + modelType: ModelType.build, + modelId: record.build, + hidden: !user.hasViewRole(UserRoles.build), + navigate: navigate + }) + ]; + }, + [user] + ); + + // Control row expansion + const rowExpansion: DataTableRowExpansionProps = useMemo(() => { + return { + allowMultiple: true, + expandable: ({ record }: { record: any }) => { + // Only items with allocated stock can be expanded + return table.isRowExpanded(record.pk) || record.allocated > 0; + }, + content: ({ record }: { record: any }) => { + return ; + } + }; + }, [table.isRowExpanded]); + + return ( + <> + + + ); +} diff --git a/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx b/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx new file mode 100644 index 000000000000..5f27d649a8a2 --- /dev/null +++ b/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx @@ -0,0 +1,132 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { DataTableRowExpansionProps } from 'mantine-datatable'; +import { useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { ProgressBar } from '../../components/items/ProgressBar'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { + DescriptionColumn, + ProjectCodeColumn, + StatusColumn +} from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowViewAction } from '../RowActions'; +import RowExpansionIcon from '../RowExpansionIcon'; +import SalesOrderAllocationTable from '../sales/SalesOrderAllocationTable'; + +export default function PartSalesAllocationsTable({ + partId +}: { + partId: number; +}) { + const user = useUserState(); + const navigate = useNavigate(); + const table = useTable('part-sales-allocations'); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'order', + title: t`Sales Order`, + render: (record: any) => ( + + 0} + expanded={table.isRowExpanded(record.pk)} + /> + {record.order_detail?.reference} + + ) + }, + DescriptionColumn({ + accessor: 'order_detail.description' + }), + ProjectCodeColumn({ + accessor: 'order_detail.project_code_detail' + }), + StatusColumn({ + accessor: 'order_detail.status', + model: ModelType.salesorder, + title: t`Order Status` + }), + { + accessor: 'allocated', + title: t`Required Stock`, + render: (record: any) => ( + + ) + } + ]; + }, [table.isRowExpanded]); + + const rowActions = useCallback( + (record: any) => { + return [ + RowViewAction({ + title: t`View Sales Order`, + modelType: ModelType.salesorder, + modelId: record.order, + hidden: !user.hasViewRole(UserRoles.sales_order), + navigate: navigate + }) + ]; + }, + [user] + ); + + // Control row expansion + const rowExpansion: DataTableRowExpansionProps = useMemo(() => { + return { + allowMultiple: true, + expandable: ({ record }: { record: any }) => { + return table.isRowExpanded(record.pk) || record.allocated > 0; + }, + content: ({ record }: { record: any }) => { + return ( + + ); + } + }; + }, [table.isRowExpanded]); + + return ( + <> + + + ); +} diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx index f1a08882ac33..bfd4d1a96c20 100644 --- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx @@ -1,9 +1,7 @@ import { t } from '@lingui/macro'; -import { ActionIcon, Group, Text } from '@mantine/core'; +import { Group, Text } from '@mantine/core'; import { IconArrowRight, - IconChevronDown, - IconChevronRight, IconHash, IconShoppingCart, IconSquareArrowRight, @@ -46,6 +44,7 @@ import { RowEditAction, RowViewAction } from '../RowActions'; +import RowExpansionIcon from '../RowExpansionIcon'; import { TableHoverCard } from '../TableHoverCard'; import SalesOrderAllocationTable from './SalesOrderAllocationTable'; @@ -73,17 +72,10 @@ export default function SalesOrderLineItemTable({ render: (record: any) => { return ( - - {table.isRowExpanded(record.pk) ? ( - - ) : ( - - )} - + ); diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 0fb6f5937b03..04e946cc8869 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -212,7 +212,7 @@ test('Pages - Build Order - Allocation', async ({ page }) => { { name: 'Blue Widget', ipn: 'widget.blue', - available: '45', + available: '39', required: '5', allocated: '5' }, diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 96a20f55b7d5..b09e2680a28c 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -100,29 +100,71 @@ test('Parts - Allocations', async ({ page }) => { await doQuickLogin(page); // Let's look at the allocations for a single stock item - await page.goto(`${baseUrl}/stock/item/324/`); - await page.getByRole('tab', { name: 'Allocations' }).click(); - await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); - await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor(); - await page.getByRole('cell', { name: 'Making tables for SO 0003' }).waitFor(); + // TODO: Un-comment these lines! + // await page.goto(`${baseUrl}/stock/item/324/`); + // await page.getByRole('tab', { name: 'Allocations' }).click(); - // Let's look at the allocations for the entire part - await page.getByRole('tab', { name: 'Details' }).click(); - await page.getByRole('link', { name: 'Leg' }).click(); + // await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); + // await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor(); + // await page.getByRole('cell', { name: 'Making tables for SO 0003' }).waitFor(); - await page.getByRole('tab', { name: 'Part Details' }).click(); - await page.getByText('660 / 760').waitFor(); + // Let's look at the allocations for an entire part + await page.goto(`${baseUrl}/part/74/details`); - await page.getByRole('tab', { name: 'Allocations' }).click(); + // Check that the overall allocations are displayed correctly + await page.getByText('11 / 825').waitFor(); + await page.getByText('6 / 110').waitFor(); - // Number of table records - await page.getByText('1 - 4 / 4').waitFor(); - await page.getByRole('cell', { name: 'Making red square tables' }).waitFor(); + // Navigate to the "Allocations" tab + await page.getByRole('tab', { name: 'Allocations' }).click(); - // Navigate through to the build order - await page.getByRole('cell', { name: 'BO0007' }).click(); - await page.getByRole('tab', { name: 'Build Details' }).waitFor(); + await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); + await page.getByRole('button', { name: 'Sales Order Allocations' }).waitFor(); + + // Expected order reference values + await page.getByText('BO0001').waitFor(); + await page.getByText('BO0016').waitFor(); + await page.getByText('BO0019').waitFor(); + await page.getByText('SO0008').waitFor(); + await page.getByText('SO0025').waitFor(); + + // Check "progress" bar of BO0001 + const build_order_cell = await page.getByRole('cell', { name: 'BO0001' }); + const build_order_row = await build_order_cell + .locator('xpath=ancestor::tr') + .first(); + await build_order_row.getByText('11 / 75').waitFor(); + + // Expand allocations against BO0001 + await build_order_cell.click(); + await page.getByRole('cell', { name: '# 3', exact: true }).waitFor(); + await page.getByRole('cell', { name: 'Room 101', exact: true }).waitFor(); + await build_order_cell.click(); + + // Check row options for BO0001 + await build_order_row.getByLabel(/row-action-menu/).click(); + await page.getByRole('menuitem', { name: 'View Build Order' }).waitFor(); + await page.keyboard.press('Escape'); + + // Check "progress" bar of SO0025 + const sales_order_cell = await page.getByRole('cell', { name: 'SO0025' }); + const sales_order_row = await sales_order_cell + .locator('xpath=ancestor::tr') + .first(); + await sales_order_row.getByText('3 / 10').waitFor(); + + // Expand allocations against SO0025 + await sales_order_cell.click(); + await page.getByRole('cell', { name: '161', exact: true }); + await page.getByRole('cell', { name: '169', exact: true }); + await page.getByRole('cell', { name: '170', exact: true }); + await sales_order_cell.click(); + + // Check row options for SO0025 + await sales_order_row.getByLabel(/row-action-menu/).click(); + await page.getByRole('menuitem', { name: 'View Sales Order' }).waitFor(); + await page.keyboard.press('Escape'); }); test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {