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 }) => {