[Feature] Build allocation export (#7611)

* CUI: Add "allocated stock" panel to build order page

* Implement CUI table for build order allocations

* Add "bulk delete" option for build order allocations

* Add row actions

* Add extra fields for data export

* Add build allocation table in PUI

* Add 'batch' column

* Bump API version

* Add playwright tests

* Fix missing renderer

* Update build docs

* Update playwright tests

* Update playwright tests
This commit is contained in:
Oliver 2024-07-11 14:33:53 +10:00 committed by GitHub
parent 4e6879407e
commit 6650f3e90c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 500 additions and 68 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -26,14 +26,6 @@ To navigate to the Build Order display, select *Build* from the main navigation
{% include "img.html" %}
{% endwith %}
#### Tree View
*Tree View* also provides a tabulated view of Build Orders. Orders are displayed in a hierarchical manner, showing any parent / child relationships between different build orders.
{% with id="build_tree", url="build/build_tree.png", description="Build Tree" %}
{% include "img.html" %}
{% endwith %}
#### Calendar View
*Calendar View* shows a calendar display with upcoming build orders, based on the various dates specified for each build.
@ -121,9 +113,9 @@ The *Build Details* tab provides an overview of the Build Order:
{% include "img.html" %}
{% endwith %}
### Allocate Stock
### Line Items
The *Allocate Stock* tab provides an interface to allocate required stock (as specified by the BOM) to the build:
The *Line Items* tab provides an interface to allocate required stock (as specified by the BOM) to the build:
{% with id="build_allocate", url="build/build_allocate.png", description="Allocation tab" %}
{% include "img.html" %}
@ -131,8 +123,13 @@ The *Allocate Stock* tab provides an interface to allocate required stock (as sp
The allocation table (as shown above) shows the stock allocation progress for this build. In the example above, there are two BOM lines, which have been partially allocated.
!!! info "Completed Builds"
The *Allocate Stock* tab is not available if the build has been completed!
### Allocated Stock
The *Allocated Stock* tab displays all stock items which have been *allocated* to this build order. These stock items are reserved for this build, and will be consumed when the build is completed:
{% with id="allocated_stock_table", url="build/allocated_stock_table.png", description="Allocated Stock Table" %}
{% include "img.html" %}
{% endwith %}
### Consumed Stock

View File

@ -1,12 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 218
INVENTREE_API_VERSION = 219
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
- Adds new fields to the BuildItem API endpoints
- Adds new ordering / filtering options to the BuildItem API endpoints
v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
- Adds "can_build" field to the BomItem API

View File

@ -8,12 +8,11 @@ from django.contrib.auth.models import User
from rest_framework.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from importer.mixins import DataExportViewMixin
from InvenTree.api import MetadataView
from InvenTree.api import BulkDeleteMixin, MetadataView
from generic.states.api import StatusView
from InvenTree.helpers import str2bool, isNull
from build.status_codes import BuildStatus, BuildStatusGroups
@ -546,15 +545,17 @@ class BuildItemFilter(rest_filters.FilterSet):
return queryset.filter(install_into=None)
class BuildItemList(DataExportViewMixin, ListCreateAPI):
class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
"""API endpoint for accessing a list of BuildItem objects.
- GET: Return list of objects
- POST: Create a new BuildItem object
"""
queryset = BuildItem.objects.all()
serializer_class = build.serializers.BuildItemSerializer
filterset_class = BuildItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
def get_serializer(self, *args, **kwargs):
"""Returns a BuildItemSerializer instance based on the request."""
@ -571,7 +572,7 @@ class BuildItemList(DataExportViewMixin, ListCreateAPI):
def get_queryset(self):
"""Override the queryset method, to allow filtering by stock_item.part."""
queryset = BuildItem.objects.all()
queryset = super().get_queryset()
queryset = queryset.select_related(
'build_line',
@ -607,8 +608,25 @@ class BuildItemList(DataExportViewMixin, ListCreateAPI):
return queryset
filter_backends = [
DjangoFilterBackend,
ordering_fields = [
'part',
'sku',
'quantity',
'location',
'reference',
]
ordering_field_aliases = {
'part': 'stock_item__part__name',
'sku': 'stock_item__supplier_part__SKU',
'location': 'stock_item__location__name',
'reference': 'build_line__bom_item__reference',
}
search_fields = [
'stock_item__supplier_part__SKU',
'stock_item__part__name',
'build_line__bom_item__reference',
]

View File

@ -26,8 +26,9 @@ from stock.serializers import StockItemSerializerBrief, LocationSerializer
import common.models
from common.serializers import ProjectCodeSerializer
from importer.mixins import DataImportExportSerializerMixin
import company.serializers
import part.filters
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
import part.serializers as part_serializers
from users.serializers import OwnerSerializer
from .models import Build, BuildLine, BuildItem
@ -85,7 +86,7 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
status_text = serializers.CharField(source='get_status_display', read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
part_detail = part_serializers.PartBriefSerializer(source='part', many=False, read_only=True)
part_name = serializers.CharField(source='part.name', read_only=True, label=_('Part Name'))
@ -1062,10 +1063,13 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
# These fields are only used for data export
export_only_fields = [
'build_reference',
'bom_reference',
'sku',
'mpn',
'location_name',
'part_id',
'part_name',
'part_ipn',
'available_quantity',
]
class Meta:
@ -1085,6 +1089,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
'location_detail',
'part_detail',
'stock_item_detail',
'supplier_part_detail',
# The following fields are only used for data export
'bom_reference',
@ -1092,27 +1097,12 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
'location_name',
'mpn',
'sku',
'part_id',
'part_name',
'part_ipn',
'available_quantity',
]
# Export-only fields
sku = serializers.CharField(source='stock_item.supplier_part.SKU', label=_('Supplier Part Number'), read_only=True)
mpn = serializers.CharField(source='stock_item.supplier_part.manufacturer_part.MPN', label=_('Manufacturer Part Number'), read_only=True)
location_name = serializers.CharField(source='stock_item.location.name', label=_('Location Name'), read_only=True)
build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True)
bom_reference = serializers.CharField(source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True)
# Annotated fields
build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True)
# Extra (optional) detail fields
part_detail = PartBriefSerializer(source='stock_item.part', many=False, read_only=True, pricing=False)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location = serializers.PrimaryKeyRelatedField(source='stock_item.location', many=False, read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True)
quantity = InvenTreeDecimalField()
def __init__(self, *args, **kwargs):
"""Determine which extra details fields should be included"""
part_detail = kwargs.pop('part_detail', True)
@ -1134,6 +1124,32 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
if not build_detail:
self.fields.pop('build_detail', None)
# Export-only fields
sku = serializers.CharField(source='stock_item.supplier_part.SKU', label=_('Supplier Part Number'), read_only=True)
mpn = serializers.CharField(source='stock_item.supplier_part.manufacturer_part.MPN', label=_('Manufacturer Part Number'), read_only=True)
location_name = serializers.CharField(source='stock_item.location.name', label=_('Location Name'), read_only=True)
build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True)
bom_reference = serializers.CharField(source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True)
# Part detail fields
part_id = serializers.PrimaryKeyRelatedField(source='stock_item.part', label=_('Part ID'), many=False, read_only=True)
part_name = serializers.CharField(source='stock_item.part.name', label=_('Part Name'), read_only=True)
part_ipn = serializers.CharField(source='stock_item.part.IPN', label=_('Part IPN'), read_only=True)
# Annotated fields
build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True)
# Extra (optional) detail fields
part_detail = part_serializers.PartBriefSerializer(source='stock_item.part', many=False, read_only=True, pricing=False)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location = serializers.PrimaryKeyRelatedField(source='stock_item.location', many=False, read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True)
supplier_part_detail = company.serializers.SupplierPartSerializer(source='stock_item.supplier_part', many=False, read_only=True)
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
available_quantity = InvenTreeDecimalField(source='stock_item.quantity', read_only=True, label=_('Available Quantity'))
class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
"""Serializer for a BuildItem object."""
@ -1217,8 +1233,8 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
# Foreign key fields
bom_item_detail = BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False)
part_detail = PartSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
bom_item_detail = part_serializers.BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False)
part_detail = part_serializers.PartSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
allocations = BuildItemSerializer(many=True, read_only=True)
# Annotated (calculated) fields

View File

@ -174,7 +174,7 @@
<div class='panel panel-hidden' id='panel-allocate'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Allocate Stock to Build" %}</h4>
<h4>{% trans "Build Order Line Items" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.build.add and build.active %}
@ -231,6 +231,18 @@
</div>
</div>
<div class='panel panel-hidden' id='panel-allocated'>
<div class='panel-heading'>
<h4>{% trans "Allocated Stock" %}</h4>
</div>
<div class='panel-content'>
<div id='build-allocated-stock-toolbar'>
{% include "filter_list.html" with id='buildorderallocatedstock' %}
</div>
<table class='table table-striped table-condensed' id='allocated-stock-table' data-toolbar='#build-allocated-stock-toolbar'></table>
</div>
</div>
<div class='panel panel-hidden' id='panel-consumed'>
<div class='panel-heading'>
<h4>
@ -290,6 +302,10 @@
{% block js_ready %}
{{ block.super }}
onPanelLoad('allocated', function() {
loadBuildOrderAllocatedStockTable($('#allocated-stock-table'), {{ build.pk }});
});
onPanelLoad('consumed', function() {
loadStockTable($('#consumed-stock-table'), {
filterTarget: '#filter-list-consumed-stock',

View File

@ -5,15 +5,19 @@
{% trans "Build Order Details" as text %}
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
{% if build.is_active %}
{% trans "Allocate Stock" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
{% trans "Line Items" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-list-ol" %}
{% trans "Incomplete Outputs" as text %}
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
{% endif %}
{% trans "Completed Outputs" as text %}
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
{% if build.is_active %}
{% trans "Allocated Stock" as text %}
{% include "sidebar_item.html" with label='allocated' text=text icon="fa-list" %}
{% endif %}
{% trans "Consumed Stock" as text %}
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-list" %}
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %}
{% trans "Child Build Orders" as text %}
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
{% trans "Attachments" as text %}

View File

@ -58,6 +58,7 @@
duplicateBuildOrder,
editBuildOrder,
loadBuildLineTable,
loadBuildOrderAllocatedStockTable,
loadBuildOrderAllocationTable,
loadBuildOutputTable,
loadBuildTable,
@ -933,6 +934,180 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
}
/**
* Load a table showing all stock allocated to a given Build Order
*/
function loadBuildOrderAllocatedStockTable(table, buildId) {
let params = {
build: buildId,
part_detail: true,
location_detail: true,
stock_detail: true,
supplier_detail: true,
};
let filters = loadTableFilters('buildorderallocatedstock', params);
setupFilterList(
'buildorderallocatedstock',
$(table),
null,
{
download: true,
custom_actions: [{
label: 'actions',
actions: [{
label: 'delete',
title: '{% trans "Delete allocations" %}',
icon: 'fa-trash-alt icon-red',
permission: 'build.delete',
callback: function(data) {
constructForm('{% url "api-build-item-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete Stock Allocations" %}',
form_data: {
items: data.map(item => item.pk),
},
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
}
}]
}]
}
);
$(table).inventreeTable({
url: '{% url "api-build-item-list" %}',
queryParams: filters,
original: params,
sortable: true,
search: true,
groupBy: false,
sidePagination: 'server',
formatNoMatches: function() {
return '{% trans "No allocated stock" %}';
},
columns: [
{
title: '',
visible: true,
checkbox: true,
switchable: false,
},
{
field: 'part',
sortable: true,
switchable: false,
title: '{% trans "Part" %}',
formatter: function(value, row) {
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
}
},
{
field: 'bom_reference',
sortable: true,
switchable: true,
title: '{% trans "Reference" %}',
},
{
field: 'quantity',
sortable: true,
switchable: false,
title: '{% trans "Allocated Quantity" %}',
formatter: function(value, row) {
let stock_item = row.stock_item_detail;
let text = value;
if (stock_item.serial && stock_item.quantity == 1) {
text = `# ${stock_item.serial}`;
}
return renderLink(text, `/stock/item/${stock_item.pk}/`);
}
},
{
field: 'location',
sortable: true,
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (row.location_detail) {
return locationDetail(row, true);
}
}
},
{
field: 'install_into',
sortable: true,
title: '{% trans "Build Output" %}',
formatter: function(value, row) {
if (value) {
return renderLink(`{% trans "Stock item" %}: ${value}`, `/stock/item/${value}/`);
}
}
},
{
field: 'sku',
sortable: true,
title: '{% trans "Supplier Part" %}',
formatter: function(value, row) {
if (row.supplier_part_detail) {
let text = row.supplier_part_detail.SKU;
return renderLink(text, `/supplier-part/${row.supplier_part_detail.pk}/`);
}
}
},
{
field: 'pk',
title: '{% trans "Actions" %}',
visible: true,
switchable: false,
sortable: false,
formatter: function(value, row) {
let buttons = '';
buttons += makeEditButton('build-item-edit', row.pk, '{% trans "Edit build allocation" %}');
buttons += makeDeleteButton('build-item-delete', row.pk, '{% trans "Delete build allocation" %}');
return wrapButtons(buttons);
}
}
]
});
// Add row callbacks
$(table).on('click', '.build-item-edit', function() {
let pk = $(this).attr('pk');
constructForm(
`/api/build/item/${pk}/`,
{
fields: {
quantity: {},
},
title: '{% trans "Edit Build Allocation" %}',
refreshTable: table
}
);
});
$(table).on('click', '.build-item-delete', function() {
let pk = $(this).attr('pk');
constructForm(
`/api/build/item/${pk}/`,
{
method: 'DELETE',
title: '{% trans "Delete Build Allocation" %}',
refreshTable: table,
}
);
});
}
/**
* Load a table showing all the BuildOrder allocations for a given part
*/

View File

@ -46,3 +46,9 @@ export function RenderBuildLine({
/>
);
}
export function RenderBuildItem({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return <RenderInlineModel primary={instance.pk} />;
}

View File

@ -8,7 +8,7 @@ import { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation';
import { apiUrl } from '../../states/ApiState';
import { Thumbnail } from '../images/Thumbnail';
import { RenderBuildLine, RenderBuildOrder } from './Build';
import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build';
import {
RenderAddress,
RenderCompany,
@ -59,6 +59,7 @@ const RendererLookup: EnumDictionary<
[ModelType.address]: RenderAddress,
[ModelType.build]: RenderBuildOrder,
[ModelType.buildline]: RenderBuildLine,
[ModelType.builditem]: RenderBuildItem,
[ModelType.company]: RenderCompany,
[ModelType.contact]: RenderContact,
[ModelType.manufacturerpart]: RenderManufacturerPart,

View File

@ -113,6 +113,11 @@ export const ModelInformationDict: ModelDict = {
cui_detail: '/build/line/:pk/',
api_endpoint: ApiEndpoints.build_line_list
},
builditem: {
label: t`Build Item`,
label_multiple: t`Build Items`,
api_endpoint: ApiEndpoints.build_item_list
},
company: {
label: t`Company`,
label_multiple: t`Companies`,

View File

@ -65,6 +65,7 @@ export enum ApiEndpoints {
build_output_scrap = 'build/:id/scrap-outputs/',
build_output_delete = 'build/:id/delete-outputs/',
build_line_list = 'build/line/',
build_item_list = 'build/item/',
bom_list = 'bom/',
bom_item_validate = 'bom/:id/validate/',

View File

@ -15,6 +15,7 @@ export enum ModelType {
stockhistory = 'stockhistory',
build = 'build',
buildline = 'buildline',
builditem = 'builditem',
company = 'company',
purchaseorder = 'purchaseorder',
purchaseorderline = 'purchaseorderline',

View File

@ -7,6 +7,7 @@ import {
IconInfoCircle,
IconList,
IconListCheck,
IconListNumbers,
IconNotes,
IconPaperclip,
IconQrcode,
@ -45,6 +46,7 @@ import {
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
import BuildLineTable from '../../tables/build/BuildLineTable';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import BuildOutputTable from '../../tables/build/BuildOutputTable';
@ -233,9 +235,9 @@ export default function BuildDetail() {
content: detailsPanel
},
{
name: 'allocate-stock',
label: t`Allocate Stock`,
icon: <IconListCheck />,
name: 'line-items',
label: t`Line Items`,
icon: <IconListNumbers />,
content: build?.pk ? (
<BuildLineTable
params={{
@ -268,10 +270,20 @@ export default function BuildDetail() {
/>
)
},
{
name: 'allocated-stock',
label: t`Allocated Stock`,
icon: <IconList />,
content: build.pk ? (
<BuildAllocatedStockTable buildId={build.pk} />
) : (
<Skeleton />
)
},
{
name: 'consumed-stock',
label: t`Consumed Stock`,
icon: <IconList />,
icon: <IconListCheck />,
content: (
<StockItemTable
allowAdd={false}

View File

@ -0,0 +1,158 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { LocationColumn, PartColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Render a table of allocated stock for a build.
*/
export default function BuildAllocatedStockTable({
buildId
}: {
buildId: number;
}) {
const user = useUserState();
const table = useTable('build-allocated-stock');
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'tracked',
label: t`Allocated to Output`,
description: t`Show items allocated to a build output`
}
];
}, []);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'part',
title: t`Part`,
sortable: true,
switchable: false,
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'bom_reference',
title: t`Reference`,
sortable: true,
switchable: true
},
{
accessor: 'quantity',
title: t`Allocated Quantity`,
sortable: true,
switchable: false
},
{
accessor: 'batch',
title: t`Batch Code`,
sortable: false,
switchable: true,
render: (record: any) => record?.stock_item_detail?.batch
},
{
accessor: 'available',
title: t`Available Quantity`,
render: (record: any) => record?.stock_item_detail?.quantity
},
LocationColumn({
accessor: 'location_detail',
switchable: true,
sortable: true
}),
{
accessor: 'install_into',
title: t`Build Output`,
sortable: true
},
{
accessor: 'sku',
title: t`Supplier Part`,
render: (record: any) => record?.supplier_part_detail?.SKU,
sortable: true
}
];
}, []);
const [selectedItem, setSelectedItem] = useState<number>(0);
const editItem = useEditApiFormModal({
pk: selectedItem,
url: ApiEndpoints.build_item_list,
title: t`Edit Build Item`,
fields: {
quantity: {}
},
table: table
});
const deleteItem = useDeleteApiFormModal({
pk: selectedItem,
url: ApiEndpoints.build_item_list,
title: t`Delete Build Item`,
table: table
});
const rowActions = useCallback(
(record: any) => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.build),
onClick: () => {
setSelectedItem(record.pk);
editItem.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.build),
onClick: () => {
setSelectedItem(record.pk);
deleteItem.open();
}
})
];
},
[user]
);
return (
<>
{editItem.modal}
{deleteItem.modal}
<InvenTreeTable
tableState={table}
url={apiUrl(ApiEndpoints.build_item_list)}
columns={tableColumns}
props={{
params: {
build: buildId,
part_detail: true,
location_detail: true,
stock_detail: true,
supplier_detail: true
},
enableBulkDelete: true,
enableDownload: true,
enableSelection: true,
rowActions: rowActions,
tableFilters: tableFilters
}}
/>
</>
);
}

View File

@ -0,0 +1,34 @@
import { test } from '../baseFixtures.ts';
import { baseUrl } from '../defaults.ts';
import { doQuickLogin } from '../login.ts';
test('PUI - Pages - Build Order', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`);
// Navigate to the correct build order
await page.getByRole('tab', { name: 'Build', exact: true }).click();
await page.getByRole('cell', { name: 'BO0011' }).click();
// Click on some tabs
await page.getByRole('tab', { name: 'Attachments' }).click();
await page.getByRole('tab', { name: 'Notes' }).click();
await page.getByRole('tab', { name: 'Incomplete Outputs' }).click();
await page.getByRole('tab', { name: 'Line Items' }).click();
await page.getByRole('tab', { name: 'Allocated Stock' }).click();
// Check for expected text in the table
await page.getByText('R_10R_0402_1%').click();
await page
.getByRole('cell', { name: 'R38, R39, R40, R41, R42, R43' })
.click();
// Click through to the "parent" build
await page.getByRole('tab', { name: 'Build Details' }).click();
await page.getByRole('link', { name: 'BO0010' }).click();
await page
.getByLabel('Build Details')
.getByText('Making a high level assembly')
.waitFor();
});

View File

@ -116,8 +116,6 @@ test('PUI - Pages - Part - Pricing (Variant)', async ({ page }) => {
// Variant Pricing
await page.getByRole('button', { name: 'Variant Pricing' }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: 'Variant Part Not sorted' }).click();
// Variant Pricing - linkjumping
let target = page.getByText('Green Chair').first();

View File

@ -32,20 +32,6 @@ test('PUI - Stock', async ({ page }) => {
await page.getByRole('tab', { name: 'Installed Items' }).click();
});
test('PUI - Build', async ({ page }) => {
await doQuickLogin(page);
await page.getByRole('tab', { name: 'Build' }).click();
await page.getByText('Widget Assembly Variant').click();
await page.getByRole('tab', { name: 'Allocate Stock' }).click();
await page.getByRole('tab', { name: 'Incomplete Outputs' }).click();
await page.getByRole('tab', { name: 'Completed Outputs' }).click();
await page.getByRole('tab', { name: 'Consumed Stock' }).click();
await page.getByRole('tab', { name: 'Child Build Orders' }).click();
await page.getByRole('tab', { name: 'Attachments' }).click();
await page.getByRole('tab', { name: 'Notes' }).click();
});
test('PUI - Purchasing', async ({ page }) => {
await doQuickLogin(page);