mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-18 18:56:31 +00:00
[UI] Part requirements (#10036)
* Tweak SalesOrderAllocations table * Refactor "include_variants" filter * Improved API filtering for "SalesOrderLineItem" endpoint * Fetch part detail for table * Fix email template - Referenced template which does not exist * Refactor the "requirements" endpoint - Include variant part requirements too * Updated starred notifications * Adjust column name * Update PartDetail - Extract information from partRequirements query first * Cache BOM items * Improve PartDetail page * Enhance isGeneratingSchema - Call inspect as late as possible * Adjust PartDetail * Improve BuildOrderAllocations table * Exclude common.newsfeedentry when exporting * Updated playwright tests * Bump API version
This commit is contained in:
@@ -3,7 +3,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||
import { IconBell } from '@tabler/icons-react';
|
||||
import type { JSX } from 'react';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
@@ -31,8 +31,10 @@ export default function StarredToggleButton({
|
||||
{ starred: !starred }
|
||||
)
|
||||
.then(() => {
|
||||
hideNotification('subscription-update');
|
||||
showNotification({
|
||||
title: 'Subscription updated',
|
||||
title: t`Subscription Updated`,
|
||||
id: 'subscription-update',
|
||||
message: `Subscription ${starred ? 'removed' : 'added'}`,
|
||||
autoClose: 5000,
|
||||
color: 'blue'
|
||||
|
@@ -153,6 +153,16 @@ export default function PartDetail() {
|
||||
|
||||
const data = { ...part };
|
||||
|
||||
const fetching =
|
||||
partRequirementsQuery.isFetching || instanceQuery.isFetching;
|
||||
|
||||
// Copy part requirements data into the main part data
|
||||
data.total_in_stock =
|
||||
partRequirements?.total_stock ?? part?.total_in_stock ?? 0;
|
||||
data.unallocated =
|
||||
partRequirements?.unallocated_stock ?? part?.unallocated_stock ?? 0;
|
||||
data.ordering = partRequirements?.ordering ?? part?.ordering ?? 0;
|
||||
|
||||
data.required =
|
||||
(partRequirements?.required_for_build_orders ??
|
||||
part?.required_for_build_orders ??
|
||||
@@ -273,26 +283,12 @@ export default function PartDetail() {
|
||||
label: t`In Stock`
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
type: 'progressbar',
|
||||
name: 'unallocated_stock',
|
||||
unit: part.units,
|
||||
total: data.total_in_stock,
|
||||
progress: data.unallocated,
|
||||
label: t`Available Stock`,
|
||||
hidden: part.total_in_stock == part.unallocated_stock
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'variant_stock',
|
||||
unit: part.units,
|
||||
label: t`Variant Stock`,
|
||||
hidden: !part.variant_stock,
|
||||
icon: 'stock'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'minimum_stock',
|
||||
unit: part.units,
|
||||
label: t`Minimum Stock`,
|
||||
hidden: part.minimum_stock <= 0
|
||||
hidden: data.total_in_stock == data.unallocated
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
@@ -306,47 +302,56 @@ export default function PartDetail() {
|
||||
name: 'required',
|
||||
label: t`Required for Orders`,
|
||||
unit: part.units,
|
||||
hidden: part.required <= 0,
|
||||
hidden: data.required <= 0,
|
||||
icon: 'stocktake'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'allocated_to_build_orders',
|
||||
icon: 'tick_off',
|
||||
total: part.required_for_build_orders,
|
||||
progress: part.allocated_to_build_orders,
|
||||
icon: 'manufacturers',
|
||||
total: partRequirements.required_for_build_orders,
|
||||
progress: partRequirements.allocated_to_build_orders,
|
||||
label: t`Allocated to Build Orders`,
|
||||
hidden:
|
||||
!part.component ||
|
||||
(part.required_for_build_orders <= 0 &&
|
||||
part.allocated_to_build_orders <= 0)
|
||||
fetching ||
|
||||
(partRequirements.required_for_build_orders <= 0 &&
|
||||
partRequirements.allocated_to_build_orders <= 0)
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
icon: 'tick_off',
|
||||
icon: 'sales_orders',
|
||||
name: 'allocated_to_sales_orders',
|
||||
total: part.required_for_sales_orders,
|
||||
progress: part.allocated_to_sales_orders,
|
||||
total: partRequirements.required_for_sales_orders,
|
||||
progress: partRequirements.allocated_to_sales_orders,
|
||||
label: t`Allocated to Sales Orders`,
|
||||
hidden:
|
||||
!part.salable ||
|
||||
(part.required_for_sales_orders <= 0 &&
|
||||
part.allocated_to_sales_orders <= 0)
|
||||
fetching ||
|
||||
(partRequirements.required_for_sales_orders <= 0 &&
|
||||
partRequirements.allocated_to_sales_orders <= 0)
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'building',
|
||||
label: t`In Production`,
|
||||
progress: part.building,
|
||||
total: part.scheduled_to_build,
|
||||
hidden: !part.assembly || (!part.building && !part.scheduled_to_build)
|
||||
progress: partRequirements.building,
|
||||
total: partRequirements.scheduled_to_build,
|
||||
hidden:
|
||||
fetching ||
|
||||
(!partRequirements.building && !partRequirements.scheduled_to_build)
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'can_build',
|
||||
unit: part.units,
|
||||
label: t`Can Build`,
|
||||
hidden: !part.assembly || partRequirementsQuery.isFetching
|
||||
hidden: !part.assembly || fetching
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'minimum_stock',
|
||||
unit: part.units,
|
||||
label: t`Minimum Stock`,
|
||||
hidden: part.minimum_stock <= 0
|
||||
}
|
||||
];
|
||||
|
||||
@@ -768,30 +773,37 @@ export default function PartDetail() {
|
||||
}, [part]);
|
||||
|
||||
const badges: ReactNode[] = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
if (partRequirementsQuery.isFetching) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const required =
|
||||
part.required_for_build_orders + part.required_for_sales_orders;
|
||||
partRequirements.required_for_build_orders +
|
||||
partRequirements.required_for_sales_orders;
|
||||
|
||||
return [
|
||||
<DetailsBadge
|
||||
label={`${t`In Stock`}: ${part.total_in_stock}`}
|
||||
color={part.total_in_stock >= part.minimum_stock ? 'green' : 'orange'}
|
||||
visible={part.total_in_stock > 0}
|
||||
label={`${t`In Stock`}: ${partRequirements.total_stock}`}
|
||||
color={
|
||||
partRequirements.total_stock >= part.minimum_stock
|
||||
? 'green'
|
||||
: 'orange'
|
||||
}
|
||||
visible={partRequirements.total_stock > 0}
|
||||
key='in_stock'
|
||||
/>,
|
||||
<DetailsBadge
|
||||
label={`${t`Available`}: ${part.unallocated_stock}`}
|
||||
label={`${t`Available`}: ${partRequirements.unallocated_stock}`}
|
||||
color='yellow'
|
||||
key='available_stock'
|
||||
visible={part.unallocated_stock != part.total_in_stock}
|
||||
visible={
|
||||
partRequirements.unallocated_stock != partRequirements.total_stock
|
||||
}
|
||||
/>,
|
||||
<DetailsBadge
|
||||
label={t`No Stock`}
|
||||
color='orange'
|
||||
visible={part.total_in_stock == 0}
|
||||
visible={partRequirements.total_stock == 0}
|
||||
key='no_stock'
|
||||
/>,
|
||||
<DetailsBadge
|
||||
@@ -801,15 +813,15 @@ export default function PartDetail() {
|
||||
key='required'
|
||||
/>,
|
||||
<DetailsBadge
|
||||
label={`${t`On Order`}: ${part.ordering}`}
|
||||
label={`${t`On Order`}: ${partRequirements.ordering}`}
|
||||
color='blue'
|
||||
visible={part.ordering > 0}
|
||||
visible={partRequirements.ordering > 0}
|
||||
key='on_order'
|
||||
/>,
|
||||
<DetailsBadge
|
||||
label={`${t`In Production`}: ${part.building}`}
|
||||
label={`${t`In Production`}: ${partRequirements.building}`}
|
||||
color='blue'
|
||||
visible={part.building > 0}
|
||||
visible={partRequirements.building > 0}
|
||||
key='in_production'
|
||||
/>,
|
||||
<DetailsBadge
|
||||
@@ -819,7 +831,7 @@ export default function PartDetail() {
|
||||
key='inactive'
|
||||
/>
|
||||
];
|
||||
}, [part, instanceQuery.isFetching]);
|
||||
}, [partRequirements, partRequirementsQuery.isFetching, part]);
|
||||
|
||||
const partFields = usePartFields({ create: false });
|
||||
|
||||
|
@@ -249,6 +249,15 @@ export function HasProjectCodeFilter(): TableFilter {
|
||||
};
|
||||
}
|
||||
|
||||
export function IncludeVariantsFilter(): TableFilter {
|
||||
return {
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include results for part variants`
|
||||
};
|
||||
}
|
||||
|
||||
export function OrderStatusFilter({
|
||||
model
|
||||
}: { model: ModelType }): TableFilter {
|
||||
|
@@ -26,7 +26,7 @@ import {
|
||||
ReferenceColumn,
|
||||
StatusColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { StockLocationFilter } from '../Filter';
|
||||
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
/**
|
||||
@@ -66,12 +66,7 @@ export default function BuildAllocatedStockTable({
|
||||
];
|
||||
|
||||
if (!!partId) {
|
||||
filters.push({
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include orders for part variants`
|
||||
});
|
||||
filters.push(IncludeVariantsFilter());
|
||||
}
|
||||
|
||||
filters.push(StockLocationFilter());
|
||||
|
@@ -33,6 +33,7 @@ import {
|
||||
CreatedAfterFilter,
|
||||
CreatedBeforeFilter,
|
||||
HasProjectCodeFilter,
|
||||
IncludeVariantsFilter,
|
||||
IssuedByFilter,
|
||||
MaxDateFilter,
|
||||
MinDateFilter,
|
||||
@@ -190,12 +191,7 @@ export function BuildOrderTable({
|
||||
|
||||
// If we are filtering on a specific part, we can include the "include variants" filter
|
||||
if (!!partId) {
|
||||
filters.push({
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include orders for part variants`
|
||||
});
|
||||
filters.push(IncludeVariantsFilter());
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
@@ -285,6 +285,7 @@ export default function BuildOutputTable({
|
||||
title: t`Add Build Output`,
|
||||
modalId: 'add-build-output',
|
||||
fields: buildOutputFields,
|
||||
successMessage: t`Build output created`,
|
||||
timeout: 10000,
|
||||
initialData: {
|
||||
batch_code: build.batch,
|
||||
|
@@ -10,6 +10,7 @@ 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 type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ProjectCodeColumn,
|
||||
StatusColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { IncludeVariantsFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import RowExpansionIcon from '../RowExpansionIcon';
|
||||
import { BuildLineSubTable } from '../build/BuildLineTable';
|
||||
@@ -53,10 +55,26 @@ export default function PartBuildAllocationsTable({
|
||||
)
|
||||
},
|
||||
{
|
||||
accessor: 'part',
|
||||
accessor: 'assembly_detail',
|
||||
title: t`Assembly`,
|
||||
switchable: false,
|
||||
render: (record: any) => <PartColumn part={record.assembly_detail} />
|
||||
},
|
||||
{
|
||||
accessor: 'assembly_detail.IPN',
|
||||
title: t`Assembly IPN`
|
||||
},
|
||||
{
|
||||
accessor: 'part_detail',
|
||||
title: t`Part`,
|
||||
defaultVisible: false,
|
||||
render: (record: any) => <PartColumn part={record.part_detail} />
|
||||
},
|
||||
{
|
||||
accessor: 'part_detail.IPN',
|
||||
defaultVisible: false,
|
||||
title: t`Part IPN`
|
||||
},
|
||||
DescriptionColumn({
|
||||
accessor: 'build_detail.title'
|
||||
}),
|
||||
@@ -114,6 +132,10 @@ export default function PartBuildAllocationsTable({
|
||||
};
|
||||
}, [table.isRowExpanded]);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [IncludeVariantsFilter()];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.build_line_list)}
|
||||
@@ -124,13 +146,16 @@ export default function PartBuildAllocationsTable({
|
||||
params: {
|
||||
part: partId,
|
||||
consumable: false,
|
||||
part_detail: true,
|
||||
assembly_detail: true,
|
||||
build_detail: true,
|
||||
order_outstanding: true
|
||||
},
|
||||
enableColumnSwitching: true,
|
||||
enableSearch: false,
|
||||
rowActions: rowActions,
|
||||
rowExpansion: rowExpansion
|
||||
rowExpansion: rowExpansion,
|
||||
tableFilters: tableFilters
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@@ -32,7 +32,7 @@ import {
|
||||
NoteColumn,
|
||||
PartColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { UserFilter } from '../Filter';
|
||||
import { IncludeVariantsFilter, UserFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
@@ -142,11 +142,7 @@ export function PartParameterTable({
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'include_variants',
|
||||
label: t`Include Variants`,
|
||||
type: 'boolean'
|
||||
},
|
||||
IncludeVariantsFilter(),
|
||||
UserFilter({
|
||||
name: 'updated_by',
|
||||
label: t`Updated By`,
|
||||
|
@@ -11,7 +11,7 @@ import type { TableColumn } from '@lib/types/Tables';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { DateColumn, ReferenceColumn, StatusColumn } from '../ColumnRenderers';
|
||||
import { StatusFilterOptions } from '../Filter';
|
||||
import { IncludeVariantsFilter, StatusFilterOptions } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
@@ -133,12 +133,7 @@ export default function PartPurchaseOrdersTable({
|
||||
description: t`Filter by order status`,
|
||||
choiceFunction: StatusFilterOptions(ModelType.purchaseorder)
|
||||
},
|
||||
{
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include orders for part variants`
|
||||
}
|
||||
IncludeVariantsFilter()
|
||||
];
|
||||
}, []);
|
||||
|
||||
|
@@ -10,14 +10,17 @@ 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 type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
DescriptionColumn,
|
||||
PartColumn,
|
||||
ProjectCodeColumn,
|
||||
StatusColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { IncludeVariantsFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import RowExpansionIcon from '../RowExpansionIcon';
|
||||
import SalesOrderAllocationTable from '../sales/SalesOrderAllocationTable';
|
||||
@@ -36,6 +39,7 @@ export default function PartSalesAllocationsTable({
|
||||
{
|
||||
accessor: 'order',
|
||||
title: t`Sales Order`,
|
||||
switchable: false,
|
||||
render: (record: any) => (
|
||||
<Group wrap='nowrap' gap='xs'>
|
||||
<RowExpansionIcon
|
||||
@@ -49,6 +53,15 @@ export default function PartSalesAllocationsTable({
|
||||
DescriptionColumn({
|
||||
accessor: 'order_detail.description'
|
||||
}),
|
||||
{
|
||||
accessor: 'part_detail',
|
||||
title: t`Part`,
|
||||
render: (record: any) => <PartColumn part={record.part_detail} />
|
||||
},
|
||||
{
|
||||
accessor: 'part_detail.IPN',
|
||||
title: t`IPN`
|
||||
},
|
||||
ProjectCodeColumn({
|
||||
accessor: 'order_detail.project_code_detail'
|
||||
}),
|
||||
@@ -59,7 +72,8 @@ export default function PartSalesAllocationsTable({
|
||||
}),
|
||||
{
|
||||
accessor: 'allocated',
|
||||
title: t`Required Stock`,
|
||||
title: t`Allocated Stock`,
|
||||
switchable: false,
|
||||
render: (record: any) => (
|
||||
<ProgressBar
|
||||
progressLabel
|
||||
@@ -86,6 +100,10 @@ export default function PartSalesAllocationsTable({
|
||||
[user]
|
||||
);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [IncludeVariantsFilter()];
|
||||
}, []);
|
||||
|
||||
// Control row expansion
|
||||
const rowExpansion: DataTableRowExpansionProps<any> = useMemo(() => {
|
||||
return {
|
||||
@@ -117,11 +135,13 @@ export default function PartSalesAllocationsTable({
|
||||
minHeight: 200,
|
||||
params: {
|
||||
part: partId,
|
||||
part_detail: true,
|
||||
order_detail: true,
|
||||
order_outstanding: true
|
||||
},
|
||||
tableFilters: tableFilters,
|
||||
enableSearch: false,
|
||||
enableColumnSwitching: false,
|
||||
enableColumnSwitching: true,
|
||||
rowExpansion: rowExpansion,
|
||||
rowActions: rowActions
|
||||
}}
|
||||
|
@@ -34,6 +34,7 @@ import {
|
||||
CreatedBeforeFilter,
|
||||
CreatedByFilter,
|
||||
HasProjectCodeFilter,
|
||||
IncludeVariantsFilter,
|
||||
MaxDateFilter,
|
||||
MinDateFilter,
|
||||
OrderStatusFilter,
|
||||
@@ -93,12 +94,7 @@ export function ReturnOrderTable({
|
||||
];
|
||||
|
||||
if (!!partId) {
|
||||
filters.push({
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include orders for part variants`
|
||||
});
|
||||
filters.push(IncludeVariantsFilter());
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
@@ -32,7 +32,7 @@ import {
|
||||
ReferenceColumn,
|
||||
StatusColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { StockLocationFilter } from '../Filter';
|
||||
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
export default function SalesOrderAllocationTable({
|
||||
@@ -94,12 +94,7 @@ export default function SalesOrderAllocationTable({
|
||||
];
|
||||
|
||||
if (!!partId) {
|
||||
filters.push({
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include orders for part variants`
|
||||
});
|
||||
filters.push(IncludeVariantsFilter());
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
@@ -35,6 +35,7 @@ import {
|
||||
CreatedBeforeFilter,
|
||||
CreatedByFilter,
|
||||
HasProjectCodeFilter,
|
||||
IncludeVariantsFilter,
|
||||
MaxDateFilter,
|
||||
MinDateFilter,
|
||||
OrderStatusFilter,
|
||||
@@ -94,12 +95,7 @@ export function SalesOrderTable({
|
||||
];
|
||||
|
||||
if (!!partId) {
|
||||
filters.push({
|
||||
name: 'include_variants',
|
||||
type: 'boolean',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include orders for part variants`
|
||||
});
|
||||
filters.push(IncludeVariantsFilter());
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
@@ -32,6 +32,7 @@ import {
|
||||
import {
|
||||
BatchFilter,
|
||||
HasBatchCodeFilter,
|
||||
IncludeVariantsFilter,
|
||||
IsSerializedFilter,
|
||||
SerialFilter,
|
||||
SerialGTEFilter,
|
||||
@@ -356,11 +357,7 @@ function stockItemTableFilters({
|
||||
label: t`In Production`,
|
||||
description: t`Show items which are in production`
|
||||
},
|
||||
{
|
||||
name: 'include_variants',
|
||||
label: t`Include Variants`,
|
||||
description: t`Include stock items for variant parts`
|
||||
},
|
||||
IncludeVariantsFilter(),
|
||||
{
|
||||
name: 'consumed',
|
||||
label: t`Consumed`,
|
||||
|
@@ -187,6 +187,45 @@ test('Parts - Details', async ({ browser }) => {
|
||||
await page.getByText('2022-04-29').waitFor();
|
||||
});
|
||||
|
||||
test('Parts - Requirements', async ({ browser }) => {
|
||||
// Navigate to the "Widget Assembly" part detail page
|
||||
// This part has multiple "variants"
|
||||
// We expect that the template page includes variant requirements
|
||||
const page = await doCachedLogin(browser, { url: 'part/77/details' });
|
||||
|
||||
// Check top-level badges
|
||||
await page.getByText('In Stock: 209').waitFor();
|
||||
await page.getByText('Available: 204').waitFor();
|
||||
await page.getByText('Required: 275').waitFor();
|
||||
await page.getByText('In Production: 24').waitFor();
|
||||
|
||||
// Check requirements details
|
||||
await page.getByText('204 / 209').waitFor(); // Available stock
|
||||
await page.getByText('0 / 100').waitFor(); // Allocated to build orders
|
||||
await page.getByText('5 / 175').waitFor(); // Allocated to sales orders
|
||||
await page.getByText('24 / 214').waitFor(); // In production
|
||||
|
||||
// Let's check out the "variants" for this part, too
|
||||
await navigate(page, 'part/81/details'); // WID-REV-A
|
||||
await page.getByText('WID-REV-A', { exact: true }).first().waitFor();
|
||||
await page.getByText('In Stock: 165').waitFor();
|
||||
await page.getByText('Required: 75').waitFor();
|
||||
|
||||
await navigate(page, 'part/903/details'); // WID-REV-B
|
||||
await page.getByText('WID-REV-B', { exact: true }).first().waitFor();
|
||||
|
||||
await page.getByText('In Stock: 44').waitFor();
|
||||
await page.getByText('Available: 39').waitFor();
|
||||
await page.getByText('Required: 100').waitFor();
|
||||
await page.getByText('In Production: 10').waitFor();
|
||||
|
||||
await page.getByText('39 / 44').waitFor(); // Available stock
|
||||
await page.getByText('5 / 100').waitFor(); // Allocated to sales orders
|
||||
await page.getByText('10 / 125').waitFor(); // In production
|
||||
|
||||
await page.waitForTimeout(2500);
|
||||
});
|
||||
|
||||
test('Parts - Allocations', async ({ browser }) => {
|
||||
// Let's look at the allocations for a single stock item
|
||||
const page = await doCachedLogin(browser, { url: 'stock/item/324/' });
|
||||
|
Reference in New Issue
Block a user