2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

[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
18 changed files with 500 additions and 68 deletions

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);