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:
@ -46,3 +46,9 @@ export function RenderBuildLine({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderBuildItem({
|
||||
instance
|
||||
}: Readonly<InstanceRenderInterface>): ReactNode {
|
||||
return <RenderInlineModel primary={instance.pk} />;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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`,
|
||||
|
@ -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/',
|
||||
|
@ -15,6 +15,7 @@ export enum ModelType {
|
||||
stockhistory = 'stockhistory',
|
||||
build = 'build',
|
||||
buildline = 'buildline',
|
||||
builditem = 'builditem',
|
||||
company = 'company',
|
||||
purchaseorder = 'purchaseorder',
|
||||
purchaseorderline = 'purchaseorderline',
|
||||
|
@ -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}
|
||||
|
158
src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
Normal file
158
src/frontend/src/tables/build/BuildAllocatedStockTable.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
34
src/frontend/tests/pages/pui_build.spec.ts
Normal file
34
src/frontend/tests/pages/pui_build.spec.ts
Normal 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();
|
||||
});
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user