From 3c4db7e18e0ec0ce3dee09790a9e64451e09c5da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 09:56:33 +0000 Subject: [PATCH 01/13] Add helpers methods for table row expansion --- src/backend/InvenTree/build/serializers.py | 14 ++++++ src/frontend/src/hooks/UseTable.tsx | 10 ++++ src/frontend/src/tables/InvenTreeTable.tsx | 32 ++++++++++++- .../src/tables/build/BuildLineTable.tsx | 48 +++++++++++++++++-- 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index c7f8de18359..a76971a28e8 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -7,6 +7,7 @@ from django.db.models import ( BooleanField, Case, + Count, ExpressionWrapper, F, FloatField, @@ -1300,6 +1301,7 @@ class Meta: # Annotated fields 'allocated', + 'allocated_items', 'in_production', 'on_order', 'available_stock', @@ -1362,11 +1364,19 @@ class Meta: part_detail = part_serializers.PartBriefSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False) # Annotated (calculated) fields + + # Total quantity of allocated stock allocated = serializers.FloatField( label=_('Allocated Stock'), read_only=True ) + # Total number of individual allocations + allocated_items = serializers.IntegerField( + label=_('Allocated Items'), + read_only=True + ) + on_order = serializers.FloatField( label=_('On Order'), read_only=True @@ -1477,6 +1487,10 @@ def annotate_queryset(queryset, build=None): Sum('allocations__quantity'), 0, output_field=models.DecimalField() ), + allocated_items=Coalesce( + Count('allocations'), 0, + output_field=models.IntegerField() + ) ) ref = 'bom_item__sub_part__' diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index a68e84a1feb..e8af9ad5a51 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -23,6 +23,7 @@ export type TableState = { clearActiveFilters: () => void; expandedRecords: any[]; setExpandedRecords: (records: any[]) => void; + isRowExpanded: (pk: number) => boolean; selectedRecords: any[]; selectedIds: number[]; hasSelectedRecords: boolean; @@ -79,6 +80,14 @@ export function useTable(tableName: string): TableState { // Array of expanded records const [expandedRecords, setExpandedRecords] = useState([]); + // Function to determine if a record is expanded + const isRowExpanded = useCallback( + (pk: number) => { + return expandedRecords.includes(pk); + }, + [expandedRecords] + ); + // Array of selected records const [selectedRecords, setSelectedRecords] = useState([]); @@ -148,6 +157,7 @@ export function useTable(tableName: string): TableState { clearActiveFilters, expandedRecords, setExpandedRecords, + isRowExpanded, selectedRecords, selectedIds, setSelectedRecords, diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 5ffbc5fd33f..1f9eb01df9b 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -20,6 +20,7 @@ import { useQuery } from '@tanstack/react-query'; import { DataTable, DataTableCellClickHandler, + DataTableRowExpansionProps, DataTableSortStatus } from 'mantine-datatable'; import React, { @@ -103,7 +104,7 @@ export type InvenTreeTableProps = { barcodeActions?: React.ReactNode[]; tableFilters?: TableFilter[]; tableActions?: React.ReactNode[]; - rowExpansion?: any; + rowExpansion?: DataTableRowExpansionProps; idAccessor?: string; dataFormatter?: (data: any) => any; rowActions?: (record: T) => RowAction[]; @@ -633,6 +634,33 @@ export function InvenTreeTable>({ tableState.refreshTable(); } + /** + * Memoize row expansion options: + * - If rowExpansion is not provided, return undefined + * - Otherwise, return the rowExpansion object + * - Utilize the useTable hook to track expanded rows + */ + const rowExpansion: DataTableRowExpansionProps | undefined = + useMemo(() => { + if (!props.rowExpansion) { + return undefined; + } + + return { + ...props.rowExpansion, + expanded: { + recordIds: tableState.expandedRecords, + onRecordIdsChange: (ids: any[]) => { + tableState.setExpandedRecords(ids); + } + } + }; + }, [ + tableState.expandedRecords, + tableState.setExpandedRecords, + props.rowExpansion + ]); + const optionalParams = useMemo(() => { let optionalParamsa: Record = {}; if (tableProps.enablePagination) { @@ -779,7 +807,7 @@ export function InvenTreeTable>({ onSelectedRecordsChange={ enableSelection ? onSelectedRecordsChange : undefined } - rowExpansion={tableProps.rowExpansion} + rowExpansion={rowExpansion} rowStyle={tableProps.rowStyle} fetching={isFetching} noRecordsText={missingRecordsText} diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index bc95ee134b7..5df1a3bce05 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -1,12 +1,17 @@ import { t } from '@lingui/macro'; -import { Alert, Group, Text } from '@mantine/core'; +import { ActionIcon, Alert, Group, Space, Text } from '@mantine/core'; import { IconArrowRight, + IconChevronCompactDown, + IconChevronCompactRight, + IconChevronDown, + IconChevronRight, IconCircleMinus, IconShoppingCart, IconTool, IconWand } from '@tabler/icons-react'; +import { DataTableRowExpansionProps } from 'mantine-datatable'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -184,10 +189,30 @@ export default function BuildLineTable({ return [ { accessor: 'bom_item', + title: t`Component Part`, ordering: 'part', sortable: true, switchable: false, - render: (record: any) => PartColumn({ part: record.part_detail }) + render: (record: any) => { + const hasAllocatedItems = record?.allocated_items > 0; + + return ( + + + {table.isRowExpanded(record.pk) ? ( + + ) : ( + + )} + + + + ); + } }, { accessor: 'part_detail.IPN', @@ -280,7 +305,7 @@ export default function BuildLineTable({ } } ]; - }, [isActive]); + }, [isActive, table]); const buildOrderFields = useBuildOrderFields({ create: true }); @@ -517,6 +542,20 @@ export default function BuildLineTable({ table.selectedRecords ]); + // Control row expansion + const rowExpansion: DataTableRowExpansionProps = useMemo(() => { + return { + allowMultiple: true, + expandable: ({ record }: { record: any }) => { + // Only items with allocated stock can be expanded + return record?.allocated_items > 0; + }, + content: ({ record }: { record: any }) => { + return
hello world: {record.pk}
; + } + }; + }, []); + return ( <> {autoAllocateStock.modal} @@ -537,7 +576,8 @@ export default function BuildLineTable({ tableFilters: tableFilters, rowActions: rowActions, enableDownload: true, - enableSelection: true + enableSelection: true, + rowExpansion: rowExpansion }} /> From 8d9567c13904d21670c470b9cafa1a9bb4e3ce96 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 10:47:41 +0000 Subject: [PATCH 02/13] Render a simplified "line item sub table" - Akin to CUI implementation - But like, better... --- .../src/tables/build/BuildLineTable.tsx | 124 ++++++++++++++++-- 1 file changed, 116 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 5df1a3bce05..9652bfff77e 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, Space, Text } from '@mantine/core'; +import { ActionIcon, Alert, Group, Paper, Stack, Text } from '@mantine/core'; import { IconArrowRight, - IconChevronCompactDown, - IconChevronCompactRight, IconChevronDown, IconChevronRight, IconCircleMinus, @@ -11,7 +9,7 @@ import { IconTool, IconWand } from '@tabler/icons-react'; -import { DataTableRowExpansionProps } from 'mantine-datatable'; +import { DataTable, DataTableRowExpansionProps } from 'mantine-datatable'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -33,12 +31,112 @@ import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; -import { BooleanColumn, PartColumn } from '../ColumnRenderers'; +import { BooleanColumn, LocationColumn, PartColumn } from '../ColumnRenderers'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; -import { RowAction } from '../RowActions'; +import { + RowAction, + RowActions, + RowDeleteAction, + RowEditAction +} from '../RowActions'; import { TableHoverCard } from '../TableHoverCard'; +/** + * Render a sub-table of allocated stock against a particular build line. + * + * - Renders a simplified table of stock allocated against the build line + * - Provides "edit" and "delete" actions for each allocation + * + * Note: We expect that the "lineItem" object contains an allocations[] list + */ +function BuildLineSubTable({ + lineItem, + onEditAllocation, + onDeleteAllocation +}: { + lineItem: any; + onEditAllocation: (pk: number) => void; + onDeleteAllocation: (pk: number) => void; +}) { + const user = useUserState(); + + const tableColumns: any[] = useMemo(() => { + return [ + { + accessor: 'part', + title: t`Part`, + render: (record: any) => { + return ; + } + }, + { + accessor: 'quantity', + title: t`Quantity`, + render: (record: any) => { + if (!!record.stock_item_detail?.serial) { + return `# ${record.stock_item_detail.serial}`; + } + return record.quantity; + } + }, + { + accessor: 'stock_item_detail.batch', + title: t`Batch` + }, + LocationColumn({ + accessor: 'location_detail' + }), + { + accessor: '---actions---', + title: ' ', + width: 50, + render: (record: any) => { + return ( + { + onEditAllocation(record.pk); + } + }), + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.build), + onClick: () => { + onDeleteAllocation(record.pk); + } + }) + ]} + /> + ); + } + } + ]; + }, [user, onEditAllocation, onDeleteAllocation]); + + return ( + + + + + + ); +} + +/** + * Render a table of build lines for a particular build. + */ export default function BuildLineTable({ buildId, build, @@ -551,10 +649,20 @@ export default function BuildLineTable({ return record?.allocated_items > 0; }, content: ({ record }: { record: any }) => { - return
hello world: {record.pk}
; + return ( + { + // TODO + }} + onDeleteAllocation={(pk: number) => { + // TODO + }} + /> + ); } }; - }, []); + }, [table.refreshTable, buildId]); return ( <> From 8310afeab4c3d3049bd73e4d13be7db8ab695711 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 10:56:13 +0000 Subject: [PATCH 03/13] Edit / delete individual stock allocations --- .../tables/build/BuildAllocatedStockTable.tsx | 7 +++- .../src/tables/build/BuildLineTable.tsx | 39 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx index 379afd242e6..eeaa2ffef25 100644 --- a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx +++ b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx @@ -161,8 +161,11 @@ export default function BuildAllocatedStockTable({ const editItem = useEditApiFormModal({ pk: selectedItem, url: ApiEndpoints.build_item_list, - title: t`Edit Build Item`, + title: t`Edit Stock Allocation`, fields: { + stock_item: { + disabled: true + }, quantity: {} }, table: table @@ -171,7 +174,7 @@ export default function BuildAllocatedStockTable({ const deleteItem = useDeleteApiFormModal({ pk: selectedItem, url: ApiEndpoints.build_item_list, - title: t`Delete Build Item`, + title: t`Delete Stock Allocation`, table: table }); diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 9652bfff77e..f8bddd15bc1 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -25,7 +25,11 @@ import { import { navigateToLink } from '../../functions/navigation'; import { notYetImplemented } from '../../functions/notifications'; import { getDetailUrl } from '../../functions/urls'; -import { useCreateApiFormModal } from '../../hooks/UseForm'; +import { + useCreateApiFormModal, + useDeleteApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; import useStatusCodes from '../../hooks/UseStatusCodes'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -121,6 +125,7 @@ function BuildLineSubTable({ (0); + + const editAllocation = useEditApiFormModal({ + url: ApiEndpoints.build_item_list, + pk: selectedAllocation, + title: t`Edit Stock Allocation`, + fields: { + stock_item: { + disabled: true + }, + quantity: {} + }, + table: table + }); + + const deleteAllocation = useDeleteApiFormModal({ + url: ApiEndpoints.build_item_list, + pk: selectedAllocation, + title: t`Delete Stock Allocation`, + table: table + }); + const rowActions = useCallback( (record: any): RowAction[] => { let part = record.part_detail ?? {}; @@ -653,10 +680,12 @@ export default function BuildLineTable({ { - // TODO + setSelectedAllocation(pk); + editAllocation.open(); }} onDeleteAllocation={(pk: number) => { - // TODO + setSelectedAllocation(pk); + deleteAllocation.open(); }} /> ); @@ -670,6 +699,8 @@ export default function BuildLineTable({ {newBuildOrder.modal} {allocateStock.modal} {deallocateStock.modal} + {editAllocation.modal} + {deleteAllocation.modal} Date: Mon, 28 Oct 2024 12:06:24 +0000 Subject: [PATCH 04/13] Improvements for BuildLineTable and BuildOutputTable --- src/frontend/src/forms/BuildForms.tsx | 3 +- src/frontend/src/pages/build/BuildDetail.tsx | 6 +- .../src/tables/build/BuildLineTable.tsx | 126 ++++++++++++++---- .../src/tables/build/BuildOutputTable.tsx | 111 ++++++++++++++- 4 files changed, 213 insertions(+), 33 deletions(-) diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index a3e2044933f..0241a084104 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -512,6 +512,7 @@ export function useAllocateStockToBuildForm({ lineItems.find((item) => item.pk == row.item.build_line) ?? {}; return ( , - content: build?.pk ? ( - - ) : ( - - ) + content: build?.pk ? : }, { name: 'incomplete-outputs', diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index f8bddd15bc1..c70c407fe54 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -46,6 +46,19 @@ import { } from '../RowActions'; import { TableHoverCard } from '../TableHoverCard'; +/** + * Filter a list of allocations based on a particular output. + * - If the output is specified, only return allocations for that output + * - Otherwise, return all allocations + */ +function filterAllocationsForOutput(allocations: any[], output: any) { + if (!output?.pk) { + return allocations; + } + + return allocations.filter((a) => a.install_into == output.pk); +} + /** * Render a sub-table of allocated stock against a particular build line. * @@ -56,15 +69,27 @@ import { TableHoverCard } from '../TableHoverCard'; */ function BuildLineSubTable({ lineItem, + output, onEditAllocation, onDeleteAllocation }: { lineItem: any; + output: any; onEditAllocation: (pk: number) => void; onDeleteAllocation: (pk: number) => void; }) { const user = useUserState(); + /** + * Memoize a list of allocations for the build line. + * - If the output is specified, only show allocations for that output. + * - Otherwise, show all allocations. + */ + const allocations = useMemo( + () => filterAllocationsForOutput(lineItem.allocations, output), + [lineItem.allocations, output] + ); + const tableColumns: any[] = useMemo(() => { return [ { @@ -132,7 +157,7 @@ function BuildLineSubTable({ pinLastColumn idAccessor="pk" columns={tableColumns} - records={lineItem.allocations} + records={allocations} /> @@ -143,17 +168,17 @@ function BuildLineSubTable({ * Render a table of build lines for a particular build. */ export default function BuildLineTable({ - buildId, build, - outputId, + output, + simplified, params = {} }: Readonly<{ - buildId: number; build: any; - outputId?: number; + output?: any; + simplified?: boolean; params?: any; }>) { - const table = useTable('buildline'); + const table = useTable(simplified ? 'buildline-simplified' : 'buildline'); const user = useUserState(); const navigate = useNavigate(); const buildStatus = useStatusCodes({ modelType: ModelType.build }); @@ -288,6 +313,39 @@ export default function BuildLineTable({ ); }, []); + // Calculate the total required stock count for a given build line + const getRowRequiredCount = useCallback( + (record: any) => { + let quantity = record.quantity; + + if (output?.quantity) { + quantity = output.quantity * record.bom_item_detail.quantity; + } + + return quantity; + }, + [output] + ); + + // Calculate the total number of allocations against a particular build line and output + const getRowAllocationCount = useCallback( + (record: any) => { + const output_id = output?.pk ?? undefined; + + let count = 0; + + // Allocations matching the output ID, or all allocations if no output ID is specified + record.allocations + ?.filter((a: any) => a.install_into == output_id || !output_id) + .forEach((a: any) => { + count += a.quantity; + }); + + return count; + }, + [output] + ); + const tableColumns: TableColumn[] = useMemo(() => { return [ { @@ -297,7 +355,11 @@ export default function BuildLineTable({ sortable: true, switchable: false, render: (record: any) => { - const hasAllocatedItems = record?.allocated_items > 0; + const allocations = filterAllocationsForOutput( + record.allocations, + output + ); + const hasAllocatedItems = allocations.length > 0; return ( @@ -335,24 +397,29 @@ export default function BuildLineTable({ }, BooleanColumn({ accessor: 'bom_item_detail.optional', - ordering: 'optional' + ordering: 'optional', + hidden: simplified }), BooleanColumn({ accessor: 'bom_item_detail.consumable', - ordering: 'consumable' + ordering: 'consumable', + hidden: simplified }), BooleanColumn({ accessor: 'bom_item_detail.allow_variants', - ordering: 'allow_variants' + ordering: 'allow_variants', + hidden: simplified }), BooleanColumn({ accessor: 'bom_item_detail.inherited', ordering: 'inherited', - title: t`Gets Inherited` + title: t`Gets Inherited`, + hidden: simplified }), BooleanColumn({ accessor: 'part_detail.trackable', - ordering: 'trackable' + ordering: 'trackable', + hidden: simplified }), { accessor: 'bom_item_detail.quantity', @@ -372,12 +439,16 @@ export default function BuildLineTable({ }, { accessor: 'quantity', + title: t`Required Quantity`, sortable: true, switchable: false, render: (record: any) => { + // If a build output is specified, use the provided quantity + const quantity = getRowRequiredCount(record); + return ( - {record.quantity} + {quantity} {record?.part_detail?.units && ( [{record.part_detail.units}] )} @@ -396,19 +467,22 @@ export default function BuildLineTable({ switchable: false, hidden: !isActive, render: (record: any) => { + const required = getRowRequiredCount(record); + const allocated = getRowAllocationCount(record); + return record?.bom_item_detail?.consumable ? ( {t`Consumable item`} ) : ( ); } } ]; - }, [isActive, table]); + }, [simplified, isActive, table, output]); const buildOrderFields = useBuildOrderFields({ create: true }); @@ -459,7 +533,7 @@ export default function BuildLineTable({ const allocateStock = useAllocateStockToBuildForm({ build: build, - outputId: null, + outputId: output?.pk ?? null, buildId: build.pk, lineItems: selectedRows, onFormSuccess: () => { @@ -524,7 +598,7 @@ export default function BuildLineTable({ const in_production = build.status == buildStatus.PRODUCTION; const consumable = record.bom_item_detail?.consumable ?? false; - const hasOutput = !!outputId; + const hasOutput = !!output?.pk; // Can allocate let canAllocate = @@ -590,7 +664,7 @@ export default function BuildLineTable({ onClick: () => { setInitialData({ part: record.part, - parent: buildId, + parent: build.pk, quantity: record.quantity - record.allocated }); newBuildOrder.open(); @@ -609,13 +683,13 @@ export default function BuildLineTable({ } ]; }, - [user, outputId, build, buildStatus] + [user, output, build, buildStatus] ); const tableActions = useMemo(() => { const production = build.status == buildStatus.PRODUCTION; const canEdit = user.hasChangeRole(UserRoles.build); - const visible = production && canEdit; + const visible = production && canEdit && !simplified; return [ { // Only items with allocated stock can be expanded - return record?.allocated_items > 0; + return ( + filterAllocationsForOutput(record.allocations, output).length > 0 + ); }, content: ({ record }: { record: any }) => { return ( { setSelectedAllocation(pk); editAllocation.open(); @@ -691,7 +769,7 @@ export default function BuildLineTable({ ); } }; - }, [table.refreshTable, buildId]); + }, [output]); return ( <> @@ -708,7 +786,7 @@ export default function BuildLineTable({ props={{ params: { ...params, - build: buildId, + build: build.pk, part_detail: true }, tableActions: tableActions, diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index a377b0a741f..66433f3cce8 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -1,13 +1,25 @@ import { t } from '@lingui/macro'; -import { Group, Text } from '@mantine/core'; +import { + Divider, + Drawer, + Group, + Loader, + Paper, + Space, + Stack, + Text +} from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; import { IconCircleCheck, IconCircleX } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; +import { DataTableRowExpansionProps } from 'mantine-datatable'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '../../App'; import { ActionButton } from '../../components/buttons/ActionButton'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { ProgressBar } from '../../components/items/ProgressBar'; +import { StylishText } from '../../components/items/StylishText'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; @@ -32,12 +44,80 @@ import { LocationColumn, PartColumn, StatusColumn } from '../ColumnRenderers'; import { InvenTreeTable } from '../InvenTreeTable'; import { RowAction, RowEditAction } from '../RowActions'; import { TableHoverCard } from '../TableHoverCard'; +import BuildLineTable from './BuildLineTable'; type TestResultOverview = { name: string; result: boolean; }; +/** + * Detail drawer view for allocating stock against a specific build output + */ +function OutputAllocationDrawer({ + build, + output, + opened, + close +}: { + build: any; + output: any; + opened: boolean; + close: () => void; +}) { + return ( + <> + + {t`Build Output Stock Allocation`} + + + {output?.serial && ( + + {t`Serial Number`}: {output.serial} + + )} + {output?.batch && ( + + {t`Batch Code`}: {output.batch} + + )} + + + } + opened={opened} + onClose={close} + withCloseButton + closeOnEscape + closeOnClickOutside + styles={{ + header: { + width: '100%' + }, + title: { + width: '100%' + } + }} + > + + + + + + + ); +} + export default function BuildOutputTable({ build, refreshBuild @@ -432,6 +512,18 @@ export default function BuildOutputTable({ trackedItems ]); + const [drawerOpen, { open: openDrawer, close: closeDrawer }] = + useDisclosure(false); + + const rowExpansion: DataTableRowExpansionProps = useMemo(() => { + return { + allowMultiple: true, + content: ({ record }: { record: any }) => { + return ; + } + }; + }, [buildId]); + return ( <> {addBuildOutput.modal} @@ -439,6 +531,12 @@ export default function BuildOutputTable({ {scrapBuildOutputsForm.modal} {editBuildOutput.modal} {cancelBuildOutputsForm.modal} + { + if (hasTrackedItems && !!record.serial) { + setSelectedOutputs([record]); + openDrawer(); + } + } }} /> From 94db3898a00286d9c8a21ee75163f1de2c1b6d73 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 12:39:50 +0000 Subject: [PATCH 05/13] Improvements for table fields --- .../components/forms/fields/TableField.tsx | 106 +++++++++++++----- src/frontend/src/forms/BuildForms.tsx | 13 ++- 2 files changed, 88 insertions(+), 31 deletions(-) diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index 71de02cc5bb..bb0da08540b 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -1,7 +1,7 @@ import { Trans, t } from '@lingui/macro'; -import { Alert, Container, Group, Table } from '@mantine/core'; +import { Alert, Container, Group, Stack, Table, Text } from '@mantine/core'; import { IconExclamationCircle } from '@tabler/icons-react'; -import { useCallback, useEffect, useMemo } from 'react'; +import { ReactNode, useCallback, useEffect, useMemo } from 'react'; import { FieldValues, UseControllerReturn } from 'react-hook-form'; import { identifierString } from '../../../functions/conversion'; @@ -18,6 +18,69 @@ export interface TableFieldRowProps { removeFn: (idx: number) => void; } +function TableFieldRow({ + item, + idx, + errors, + definition, + control, + changeFn, + removeFn +}: { + item: any; + idx: number; + errors: any; + definition: ApiFormFieldType; + control: UseControllerReturn; + changeFn: (idx: number, key: string, value: any) => void; + removeFn: (idx: number) => void; +}) { + // Table fields require render function + if (!definition.modelRenderer) { + return ( + + + }> + {`modelRenderer entry required for tables`} + + + + ); + } + + return definition.modelRenderer({ + item: item, + idx: idx, + rowErrors: errors, + control: control, + changeFn: changeFn, + removeFn: removeFn + }); +} + +export function TableFieldErrorWrapper({ + props, + errorKey, + children +}: { + props: TableFieldRowProps; + errorKey: string; + children: ReactNode; +}) { + const msg = props?.rowErrors && props.rowErrors[errorKey]; + + return ( + + {children} + {msg && ( + + {msg.message} + + )} + + ); +} + export function TableField({ definition, fieldName, @@ -47,7 +110,7 @@ export function TableField({ }; // Extract errors associated with the current row - const rowErrors = useCallback( + const rowErrors: any = useCallback( (idx: number) => { if (Array.isArray(error)) { return error[idx]; @@ -74,31 +137,18 @@ export function TableField({ {value.length > 0 ? ( value.map((item: any, idx: number) => { - // Table fields require render function - if (!definition.modelRenderer) { - return ( - - - } - > - {`modelRenderer entry required for tables`} - - - - ); - } - - return definition.modelRenderer({ - item: item, - idx: idx, - rowErrors: rowErrors(idx), - control: control, - changeFn: onRowFieldChange, - removeFn: removeRow - }); + return ( + + ); }) ) : ( diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index 0241a084104..baf4998241c 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -17,7 +17,10 @@ import { ApiFormFieldSet, ApiFormFieldType } from '../components/forms/fields/ApiFormField'; -import { TableFieldRowProps } from '../components/forms/fields/TableField'; +import { + TableFieldErrorWrapper, + TableFieldRowProps +} from '../components/forms/fields/TableField'; import { ProgressBar } from '../components/items/ProgressBar'; import { StatusRenderer } from '../components/render/StatusRenderer'; import { ApiEndpoints } from '../enums/ApiEndpoints'; @@ -210,7 +213,11 @@ function BuildOutputFormRow({ - {serial} + + + {serial} + + {record.batch} {' '} @@ -259,7 +266,7 @@ export function useCompleteBuildOutputsForm({ ); }, - headers: [t`Part`, t`Stock Item`, t`Batch`, t`Status`] + headers: [t`Part`, t`Build Output`, t`Batch`, t`Status`] }, status_custom_key: {}, location: { From f61ab8e7d8e3e1c4abefe997f3c13c962878e63a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 12:52:22 +0000 Subject: [PATCH 06/13] Refactoring --- src/frontend/src/tables/build/BuildLineTable.tsx | 10 ++++------ src/frontend/src/tables/build/BuildOutputTable.tsx | 9 +++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index c70c407fe54..2697c10dd46 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -330,16 +330,14 @@ export default function BuildLineTable({ // Calculate the total number of allocations against a particular build line and output const getRowAllocationCount = useCallback( (record: any) => { - const output_id = output?.pk ?? undefined; + let allocations = filterAllocationsForOutput(record.allocations, output); let count = 0; // Allocations matching the output ID, or all allocations if no output ID is specified - record.allocations - ?.filter((a: any) => a.install_into == output_id || !output_id) - .forEach((a: any) => { - count += a.quantity; - }); + allocations.forEach((a: any) => { + count += a.quantity; + }); return count; }, diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index 66433f3cce8..c53b1418156 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -361,13 +361,18 @@ export default function BuildOutputTable({ title: t`Allocate`, tooltip: t`Allocate stock to build output`, color: 'blue', + hidden: !hasTrackedItems || !user.hasChangeRole(UserRoles.build), icon: , - onClick: notYetImplemented + onClick: () => { + setSelectedOutputs([record]); + openDrawer(); + } }, { title: t`Deallocate`, tooltip: t`Deallocate stock from build output`, color: 'red', + hidden: !hasTrackedItems || !user.hasChangeRole(UserRoles.build), icon: , onClick: notYetImplemented }, @@ -410,7 +415,7 @@ export default function BuildOutputTable({ } ]; }, - [user, partId] + [user, partId, hasTrackedItems] ); const tableColumns: TableColumn[] = useMemo(() => { From 98d340d9c552e440074d869e86164aba74b2cced Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 13:04:12 +0000 Subject: [PATCH 07/13] Refactor BuildLineTable - Calculate and cache filtered allocation values --- .../src/tables/build/BuildLineTable.tsx | 121 +++++++----------- 1 file changed, 47 insertions(+), 74 deletions(-) diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 2697c10dd46..3d71a1413ae 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -46,19 +46,6 @@ import { } from '../RowActions'; import { TableHoverCard } from '../TableHoverCard'; -/** - * Filter a list of allocations based on a particular output. - * - If the output is specified, only return allocations for that output - * - Otherwise, return all allocations - */ -function filterAllocationsForOutput(allocations: any[], output: any) { - if (!output?.pk) { - return allocations; - } - - return allocations.filter((a) => a.install_into == output.pk); -} - /** * Render a sub-table of allocated stock against a particular build line. * @@ -69,27 +56,15 @@ function filterAllocationsForOutput(allocations: any[], output: any) { */ function BuildLineSubTable({ lineItem, - output, onEditAllocation, onDeleteAllocation }: { lineItem: any; - output: any; onEditAllocation: (pk: number) => void; onDeleteAllocation: (pk: number) => void; }) { const user = useUserState(); - /** - * Memoize a list of allocations for the build line. - * - If the output is specified, only show allocations for that output. - * - Otherwise, show all allocations. - */ - const allocations = useMemo( - () => filterAllocationsForOutput(lineItem.allocations, output), - [lineItem.allocations, output] - ); - const tableColumns: any[] = useMemo(() => { return [ { @@ -157,7 +132,7 @@ function BuildLineSubTable({ pinLastColumn idAccessor="pk" columns={tableColumns} - records={allocations} + records={lineItem.filteredAllocations} /> @@ -313,37 +288,6 @@ export default function BuildLineTable({ ); }, []); - // Calculate the total required stock count for a given build line - const getRowRequiredCount = useCallback( - (record: any) => { - let quantity = record.quantity; - - if (output?.quantity) { - quantity = output.quantity * record.bom_item_detail.quantity; - } - - return quantity; - }, - [output] - ); - - // Calculate the total number of allocations against a particular build line and output - const getRowAllocationCount = useCallback( - (record: any) => { - let allocations = filterAllocationsForOutput(record.allocations, output); - - let count = 0; - - // Allocations matching the output ID, or all allocations if no output ID is specified - allocations.forEach((a: any) => { - count += a.quantity; - }); - - return count; - }, - [output] - ); - const tableColumns: TableColumn[] = useMemo(() => { return [ { @@ -353,11 +297,7 @@ export default function BuildLineTable({ sortable: true, switchable: false, render: (record: any) => { - const allocations = filterAllocationsForOutput( - record.allocations, - output - ); - const hasAllocatedItems = allocations.length > 0; + const hasAllocatedItems = record.allocatedQuantity > 0; return ( @@ -442,11 +382,9 @@ export default function BuildLineTable({ switchable: false, render: (record: any) => { // If a build output is specified, use the provided quantity - const quantity = getRowRequiredCount(record); - return ( - {quantity} + {record.requiredQuantity} {record?.part_detail?.units && ( [{record.part_detail.units}] )} @@ -465,16 +403,13 @@ export default function BuildLineTable({ switchable: false, hidden: !isActive, render: (record: any) => { - const required = getRowRequiredCount(record); - const allocated = getRowAllocationCount(record); - return record?.bom_item_detail?.consumable ? ( {t`Consumable item`} ) : ( ); } @@ -740,21 +675,58 @@ export default function BuildLineTable({ table.selectedRecords ]); + /** + * Format the records for the table, before rendering + * + * - Filter the "allocations" field to only show allocations for the selected output + * - Pre-calculate the "requiredQuantity" and "allocatedQuantity" fields + */ + const formatRecords = useCallback( + (records: any[]): any[] => { + return records.map((record) => { + let allocations = [...record.allocations]; + + // If an output is specified, filter the allocations to only show those for the selected output + if (output?.pk) { + allocations = allocations.filter((a) => a.install_into == output.pk); + } + + let allocatedQuantity = 0; + let requiredQuantity = record.quantity; + + // Calculate the total allocated quantity + allocations.forEach((a) => { + allocatedQuantity += a.quantity; + }); + + // Calculate the required quantity (based on the build output) + if (output?.quantity && record.bom_item_detail) { + requiredQuantity = output.quantity * record.bom_item_detail.quantity; + } + + return { + ...record, + filteredAllocations: allocations, + requiredQuantity: requiredQuantity, + allocatedQuantity: allocatedQuantity + }; + }); + }, + [output] + ); + // Control row expansion const rowExpansion: DataTableRowExpansionProps = useMemo(() => { return { allowMultiple: true, expandable: ({ record }: { record: any }) => { // Only items with allocated stock can be expanded - return ( - filterAllocationsForOutput(record.allocations, output).length > 0 - ); + return record.allocatedQuantity > 0; }, content: ({ record }: { record: any }) => { return ( { setSelectedAllocation(pk); editAllocation.open(); @@ -790,6 +762,7 @@ export default function BuildLineTable({ tableActions: tableActions, tableFilters: tableFilters, rowActions: rowActions, + dataFormatter: formatRecords, enableDownload: true, enableSelection: true, rowExpansion: rowExpansion From 8b785ee8f0b8e91892997af7fac4a2325f68a228 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 13:04:52 +0000 Subject: [PATCH 08/13] Code cleanup --- .../src/tables/build/BuildOutputTable.tsx | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index c53b1418156..c05497f78dc 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -1,18 +1,8 @@ import { t } from '@lingui/macro'; -import { - Divider, - Drawer, - Group, - Loader, - Paper, - Space, - Stack, - Text -} from '@mantine/core'; +import { Divider, Drawer, Group, Paper, Space, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconCircleCheck, IconCircleX } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; -import { DataTableRowExpansionProps } from 'mantine-datatable'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '../../App'; @@ -520,15 +510,6 @@ export default function BuildOutputTable({ const [drawerOpen, { open: openDrawer, close: closeDrawer }] = useDisclosure(false); - const rowExpansion: DataTableRowExpansionProps = useMemo(() => { - return { - allowMultiple: true, - content: ({ record }: { record: any }) => { - return ; - } - }; - }, [buildId]); - return ( <> {addBuildOutput.modal} @@ -555,11 +536,9 @@ export default function BuildOutputTable({ }, enableLabels: true, enableReports: true, - // modelType: ModelType.stockitem, dataFormatter: formatRecords, tableActions: tableActions, rowActions: rowActions, - // rowExpansion: rowExpansion, enableSelection: true, onRowClick: (record: any) => { if (hasTrackedItems && !!record.serial) { From 8c3bd94a9ae6c5715bae676258231250f5d294e3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 13:34:29 +0000 Subject: [PATCH 09/13] Further fixes and features --- src/frontend/src/forms/BuildForms.tsx | 6 +- .../src/tables/build/BuildLineTable.tsx | 57 ++++++++-------- .../src/tables/build/BuildOutputTable.tsx | 66 +++++++++++++++---- 3 files changed, 86 insertions(+), 43 deletions(-) diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index baf4998241c..60a24d6290f 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -461,8 +461,8 @@ function BuildAllocateLineRow({ @@ -573,7 +573,7 @@ export function useAllocateStockToBuildForm({ return { build_line: item.pk, stock_item: undefined, - quantity: Math.max(0, item.quantity - item.allocated), + quantity: Math.max(0, item.requiredQuantity - item.allocatedQuantity), output: outputId }; }) diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 3d71a1413ae..7e8335fe3f4 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -145,19 +145,20 @@ function BuildLineSubTable({ export default function BuildLineTable({ build, output, - simplified, params = {} }: Readonly<{ build: any; output?: any; - simplified?: boolean; params?: any; }>) { - const table = useTable(simplified ? 'buildline-simplified' : 'buildline'); const user = useUserState(); const navigate = useNavigate(); const buildStatus = useStatusCodes({ modelType: ModelType.build }); + const hasOutput: boolean = useMemo(() => !!output?.pk, [output]); + + const table = useTable(hasOutput ? 'buildline-output' : 'buildline'); + const isActive: boolean = useMemo(() => { return ( build?.status == buildStatus.PRODUCTION || @@ -336,28 +337,28 @@ export default function BuildLineTable({ BooleanColumn({ accessor: 'bom_item_detail.optional', ordering: 'optional', - hidden: simplified + hidden: hasOutput }), BooleanColumn({ accessor: 'bom_item_detail.consumable', ordering: 'consumable', - hidden: simplified + hidden: hasOutput }), BooleanColumn({ accessor: 'bom_item_detail.allow_variants', ordering: 'allow_variants', - hidden: simplified + hidden: hasOutput }), BooleanColumn({ accessor: 'bom_item_detail.inherited', ordering: 'inherited', title: t`Gets Inherited`, - hidden: simplified + hidden: hasOutput }), BooleanColumn({ accessor: 'part_detail.trackable', ordering: 'trackable', - hidden: simplified + hidden: hasOutput }), { accessor: 'bom_item_detail.quantity', @@ -401,6 +402,7 @@ export default function BuildLineTable({ { accessor: 'allocated', switchable: false, + sortable: true, hidden: !isActive, render: (record: any) => { return record?.bom_item_detail?.consumable ? ( @@ -415,7 +417,7 @@ export default function BuildLineTable({ } } ]; - }, [simplified, isActive, table, output]); + }, [hasOutput, isActive, table, output]); const buildOrderFields = useBuildOrderFields({ create: true }); @@ -483,12 +485,12 @@ export default function BuildLineTable({ hidden: true }, output: { - hidden: true, - value: null + hidden: true } }, initialData: { - build_line: selectedLine + build_line: selectedLine, + output: output?.pk ?? null }, preFormContent: ( @@ -622,13 +624,13 @@ export default function BuildLineTable({ const tableActions = useMemo(() => { const production = build.status == buildStatus.PRODUCTION; const canEdit = user.hasChangeRole(UserRoles.build); - const visible = production && canEdit && !simplified; + const visible = production && canEdit; return [ } tooltip={t`Auto Allocate Stock`} - hidden={!visible} + hidden={!visible || hasOutput} color="blue" onClick={() => { autoAllocateStock.open(); @@ -642,14 +644,17 @@ export default function BuildLineTable({ disabled={!table.hasSelectedRecords} color="green" onClick={() => { - setSelectedRows( - table.selectedRecords.filter( - (r) => - r.allocated < r.quantity && - !r.trackable && - !r.bom_item_detail.consumable - ) - ); + let rows = table.selectedRecords + .filter((r) => r.allocatedQuantity < r.requiredQuantity) + .filter((r) => !r.bom_item_detail?.consumable); + + if (hasOutput) { + rows = rows.filter((r) => r.trackable); + } else { + rows = rows.filter((r) => !r.trackable); + } + + setSelectedRows(rows); allocateStock.open(); }} />, @@ -657,7 +662,7 @@ export default function BuildLineTable({ key="deallocate-stock" icon={} tooltip={t`Deallocate Stock`} - hidden={!visible} + hidden={!visible || hasOutput} disabled={table.hasSelectedRecords} color="red" onClick={() => { @@ -670,7 +675,7 @@ export default function BuildLineTable({ user, build, buildStatus, - simplified, + hasOutput, table.hasSelectedRecords, table.selectedRecords ]); @@ -721,7 +726,7 @@ export default function BuildLineTable({ allowMultiple: true, expandable: ({ record }: { record: any }) => { // Only items with allocated stock can be expanded - return record.allocatedQuantity > 0; + return table.isRowExpanded(record.pk) || record.allocatedQuantity > 0; }, content: ({ record }: { record: any }) => { return ( @@ -739,7 +744,7 @@ export default function BuildLineTable({ ); } }; - }, [output]); + }, [table.isRowExpanded, output]); return ( <> diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index c05497f78dc..d5587a356e9 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -1,7 +1,19 @@ import { t } from '@lingui/macro'; -import { Divider, Drawer, Group, Paper, Space, Text } from '@mantine/core'; +import { + Alert, + Divider, + Drawer, + Group, + Paper, + Space, + Text +} from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { IconCircleCheck, IconCircleX } from '@tabler/icons-react'; +import { + IconCircleCheck, + IconCircleX, + IconExclamationCircle +} from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -21,7 +33,6 @@ import { } from '../../forms/BuildForms'; import { useStockFields } from '../../forms/StockForms'; import { InvenTreeIcon } from '../../functions/icons'; -import { notYetImplemented } from '../../functions/notifications'; import { useCreateApiFormModal, useEditApiFormModal @@ -95,7 +106,6 @@ function OutputAllocationDrawer({ { if (!buildId || buildId < 0) { @@ -181,7 +191,7 @@ export default function BuildOutputTable({ // Ensure base table data is updated correctly useEffect(() => { table.refreshTable(); - }, [hasTrackedItems, hasRequiredTests]); + }, [testTemplates, trackedItems, hasTrackedItems, hasRequiredTests]); // Format table records const formatRecords = useCallback( @@ -222,13 +232,11 @@ export default function BuildOutputTable({ let allocated = 0; // Find all allocations which match the build output - let allocations = item.allocations.filter( - (allocation: any) => allocation.install_into == record.pk - ); - - allocations.forEach((allocation: any) => { - allocated += allocation.quantity; - }); + item.allocations + ?.filter((allocation: any) => allocation.install_into == record.pk) + ?.forEach((allocation: any) => { + allocated += allocation.quantity; + }); if (allocated >= item.bom_item_detail.quantity) { fullyAllocatedCount += 1; @@ -300,6 +308,32 @@ export default function BuildOutputTable({ table: table }); + const deallocateBuildOutput = useCreateApiFormModal({ + url: ApiEndpoints.build_order_deallocate, + pk: build.pk, + title: t`Deallocate Stock`, + preFormContent: ( + } + title={t`Deallocate Stock`} + > + {t`This action will deallocate all stock from the selected build output`} + + ), + fields: { + output: { + hidden: true + } + }, + initialData: { + output: selectedOutputs[0]?.pk + }, + onFormSuccess: () => { + refetchTrackedItems(); + } + }); + const tableActions = useMemo(() => { return [ , - onClick: notYetImplemented + onClick: () => { + setSelectedOutputs([record]); + deallocateBuildOutput.open(); + } }, { title: t`Complete`, @@ -516,6 +553,7 @@ export default function BuildOutputTable({ {completeBuildOutputsForm.modal} {scrapBuildOutputsForm.modal} {editBuildOutput.modal} + {deallocateBuildOutput.modal} {cancelBuildOutputsForm.modal} Date: Mon, 28 Oct 2024 14:06:06 +0000 Subject: [PATCH 10/13] Revert new serializer field - Turns out not to be needed --- src/backend/InvenTree/build/serializers.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index a76971a28e8..85053dabf4b 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -1301,7 +1301,6 @@ class Meta: # Annotated fields 'allocated', - 'allocated_items', 'in_production', 'on_order', 'available_stock', @@ -1371,12 +1370,6 @@ class Meta: read_only=True ) - # Total number of individual allocations - allocated_items = serializers.IntegerField( - label=_('Allocated Items'), - read_only=True - ) - on_order = serializers.FloatField( label=_('On Order'), read_only=True @@ -1486,10 +1479,6 @@ def annotate_queryset(queryset, build=None): allocated=Coalesce( Sum('allocations__quantity'), 0, output_field=models.DecimalField() - ), - allocated_items=Coalesce( - Count('allocations'), 0, - output_field=models.IntegerField() ) ) From cb64cabf2fa590d8930ece541da0a158e9ea117e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 14:35:46 +0000 Subject: [PATCH 11/13] Add playwright tests --- src/frontend/tests/pages/pui_build.spec.ts | 100 +++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index faaea12fbca..0fb6f5937b0 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -157,3 +157,103 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => { await page.getByRole('button', { name: 'Submit' }).click(); await page.getByText('Build outputs have been completed').waitFor(); }); + +test('Pages - Build Order - Allocation', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/manufacturing/build-order/1/line-items`); + + // Expand the R_10K_0805 line item + await page.getByText('R_10K_0805_1%').first().click(); + await page.getByText('Reel Storage').waitFor(); + await page.getByText('R_10K_0805_1%').first().click(); + + // The capacitor stock should be fully allocated + const cell = await page.getByRole('cell', { name: /C_1uF_0805/ }); + const row = await cell.locator('xpath=ancestor::tr').first(); + + await row.getByText(/150 \/ 150/).waitFor(); + + // Expand this row + await cell.click(); + await page.getByRole('cell', { name: '2022-4-27', exact: true }).waitFor(); + await page.getByRole('cell', { name: 'Reel Storage', exact: true }).waitFor(); + + // Navigate to the "Incomplete Outputs" tab + await page.getByRole('tab', { name: 'Incomplete Outputs' }).click(); + + // Find output #7 + const output7 = await page + .getByRole('cell', { name: '# 7' }) + .locator('xpath=ancestor::tr') + .first(); + + // Expecting 3/4 allocated outputs + await output7.getByText('3 / 4').waitFor(); + + // Expecting 0/3 completed tests + await output7.getByText('0 / 3').waitFor(); + + // Expand the output + await output7.click(); + + await page.getByText('Build Output Stock Allocation').waitFor(); + await page.getByText('Serial Number: 7').waitFor(); + + // Data of expected rows + const data = [ + { + name: 'Red Widget', + ipn: 'widget.red', + available: '123', + required: '3', + allocated: '3' + }, + { + name: 'Blue Widget', + ipn: 'widget.blue', + available: '45', + required: '5', + allocated: '5' + }, + { + name: 'Pink Widget', + ipn: 'widget.pink', + available: '4', + required: '4', + allocated: '0' + }, + { + name: 'Green Widget', + ipn: 'widget.green', + available: '245', + required: '6', + allocated: '6' + } + ]; + + // Check for expected rows + for (let idx = 0; idx < data.length; idx++) { + let item = data[idx]; + + let cell = await page.getByRole('cell', { name: item.name }); + let row = await cell.locator('xpath=ancestor::tr').first(); + let progress = `${item.allocated} / ${item.required}`; + + await row.getByRole('cell', { name: item.ipn }).first().waitFor(); + await row.getByRole('cell', { name: item.available }).first().waitFor(); + await row.getByRole('cell', { name: progress }).first().waitFor(); + } + + // Check for expected buttons on Red Widget + let redWidget = await page.getByRole('cell', { name: 'Red Widget' }); + let redRow = await redWidget.locator('xpath=ancestor::tr').first(); + + await redRow.getByLabel(/row-action-menu-/i).click(); + await page + .getByRole('menuitem', { name: 'Allocate Stock', exact: true }) + .waitFor(); + await page + .getByRole('menuitem', { name: 'Deallocate Stock', exact: true }) + .waitFor(); +}); From f256cb6f4ec4d6bd14083e3bb1533a953959c1f0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 23:05:56 +0000 Subject: [PATCH 12/13] Bug fix for CUI tables - Ensure allocations are correctly filtered by output ID --- src/backend/InvenTree/templates/js/translated/build.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/templates/js/translated/build.js b/src/backend/InvenTree/templates/js/translated/build.js index a16d93e4ab0..07fd1cf24d5 100644 --- a/src/backend/InvenTree/templates/js/translated/build.js +++ b/src/backend/InvenTree/templates/js/translated/build.js @@ -2505,6 +2505,7 @@ function renderBuildLineAllocationTable(element, build_line, options={}) { url: '{% url "api-build-item-list" %}', queryParams: { build_line: build_line.pk, + output: options.output ?? undefined, }, showHeader: false, columns: [ @@ -2609,9 +2610,10 @@ function renderBuildLineAllocationTable(element, build_line, options={}) { */ function loadBuildLineTable(table, build_id, options={}) { + const params = options.params || {}; + const output = options.output; + let name = 'build-lines'; - let params = options.params || {}; - let output = options.output; params.build = build_id; @@ -2647,6 +2649,7 @@ function loadBuildLineTable(table, build_id, options={}) { detailFormatter: function(_index, row, element) { renderBuildLineAllocationTable(element, row, { parent_table: table, + output: output, }); }, formatNoMatches: function() { From 3483c29139e44a7d55fcb9ebc4f5d978bafba0f0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Oct 2024 23:23:59 +0000 Subject: [PATCH 13/13] Adjust CUI table --- src/backend/InvenTree/templates/js/translated/build.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/InvenTree/templates/js/translated/build.js b/src/backend/InvenTree/templates/js/translated/build.js index 07fd1cf24d5..4e6ec08c62b 100644 --- a/src/backend/InvenTree/templates/js/translated/build.js +++ b/src/backend/InvenTree/templates/js/translated/build.js @@ -2733,6 +2733,15 @@ function loadBuildLineTable(table, build_id, options={}) { return yesNoLabel(row.bom_item_detail.inherited); } }, + { + field: 'trackable', + title: '{% trans "Trackable" %}', + sortable: true, + switchable: true, + formatter: function(value, row) { + return yesNoLabel(row.part_detail.trackable); + } + }, { field: 'unit_quantity', sortable: true,