From f271725defc7360635d7bb8aa2594f5bb2bfe8df Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 26 Apr 2026 16:32:58 +1000 Subject: [PATCH] [UI] BOM row expand (#11809) * Add subassembly expansion Co-authored-by: Copilot * Enable multi-level subassembly display Co-authored-by: Copilot * Adjust padding * Adds user setting to enable / disable subassembly view Co-authored-by: Copilot * Add icon * Additional playwright tests Co-authored-by: Copilot --------- Co-authored-by: Copilot --- CHANGELOG.md | 1 + docs/docs/settings/user.md | 1 + src/backend/InvenTree/common/setting/user.py | 6 + .../src/pages/Index/Settings/UserSettings.tsx | 3 +- .../src/tables/bom/BomSubassemblyTable.tsx | 118 ++++++++++++++++++ src/frontend/src/tables/bom/BomTable.tsx | 36 +++++- src/frontend/tests/pages/pui_part.spec.ts | 5 + 7 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/frontend/src/tables/bom/BomSubassemblyTable.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index d36d4c7cd5..eee68fb4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [#11809](https://github.com/inventree/InvenTree/pull/11809) adds multi-level subassembly display mode to the BOM table, allowing users to view multiple levels of subassemblies in a single table view. This is an optional display mode which can be toggled on or off by the user. - [#11778](https://github.com/inventree/InvenTree/pull/11778) adds inline supplier part creation to po line item addition dialog. - [#11772](https://github.com/inventree/InvenTree/pull/11772) the UI now warns if you navigate away from a note panel with unsaved changes - [#11788](https://github.com/inventree/InvenTree/pull/11788) adds support for custom permissions checks on database models defined in plugins. If a model defines a `check_user_permission` classmethod, this will be called to determine if a user has permission to view the model. This is required for plugin models which do not have the required ruleset definitions for the standard permission system. diff --git a/docs/docs/settings/user.md b/docs/docs/settings/user.md index faaa7aae8a..bd862cd695 100644 --- a/docs/docs/settings/user.md +++ b/docs/docs/settings/user.md @@ -27,6 +27,7 @@ The *Display Settings* screen shows general display configuration options: {{ usersetting("FORMS_CLOSE_USING_ESCAPE") }} {{ usersetting("DISPLAY_STOCKTAKE_TAB") }} {{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}} +{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}} {{ usersetting("ENABLE_LAST_BREADCRUMB") }} {{ usersetting("SHOW_FULL_LOCATION_IN_TABLES") }} diff --git a/src/backend/InvenTree/common/setting/user.py b/src/backend/InvenTree/common/setting/user.py index 23dee7e5bd..57a117e20e 100644 --- a/src/backend/InvenTree/common/setting/user.py +++ b/src/backend/InvenTree/common/setting/user.py @@ -251,6 +251,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'SHOW_BOM_SUBASSEMBLY_LEVELS': { + 'name': _('Show Subassemblies in BOM table'), + 'description': _('Enable display of subassemblies in the BOM table'), + 'default': True, + 'validator': bool, + }, 'NOTIFICATION_ERROR_REPORT': { 'name': _('Receive error reports'), 'description': _('Receive notifications for system errors'), diff --git a/src/frontend/src/pages/Index/Settings/UserSettings.tsx b/src/frontend/src/pages/Index/Settings/UserSettings.tsx index a30397e373..f0a0019f85 100644 --- a/src/frontend/src/pages/Index/Settings/UserSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/UserSettings.tsx @@ -66,7 +66,8 @@ export default function UserSettings() { 'DISPLAY_STOCKTAKE_TAB', 'ENABLE_LAST_BREADCRUMB', 'SHOW_FULL_LOCATION_IN_TABLES', - 'SHOW_FULL_CATEGORY_IN_TABLES' + 'SHOW_FULL_CATEGORY_IN_TABLES', + 'SHOW_BOM_SUBASSEMBLY_LEVELS' ]} /> ) diff --git a/src/frontend/src/tables/bom/BomSubassemblyTable.tsx b/src/frontend/src/tables/bom/BomSubassemblyTable.tsx new file mode 100644 index 0000000000..71373e599d --- /dev/null +++ b/src/frontend/src/tables/bom/BomSubassemblyTable.tsx @@ -0,0 +1,118 @@ +import { ApiEndpoints, ModelType, apiUrl, useTable } from '@lib/index'; +import type { TableColumn, TableState } from '@lib/types/Tables'; +import { Group, Paper } from '@mantine/core'; +import { IconCornerLeftUp } from '@tabler/icons-react'; +import { useMemo } from 'react'; +import Expand from '../../components/items/Expand'; +import { useUserSettingsState } from '../../states/SettingsStates'; +import { + IPNColumn, + ReferenceColumn, + RenderPartColumn +} from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; +import RowExpansionIcon from '../RowExpansionIcon'; + +export function subassemblyRowExpansion({ + table +}: { + table: TableState; +}) { + const userSettings = useUserSettingsState(); + + return useMemo(() => { + // Display of sub-assemblies is optional + if (!userSettings.isSet('SHOW_BOM_SUBASSEMBLY_LEVELS')) { + return undefined; + } + + return { + allowMultiple: true, + expandable: ({ record }: { record: any }) => { + return ( + table.isRowExpanded(record.pk) || !!record.sub_part_detail?.assembly + ); + }, + content: ({ record }: { record: any }) => { + return ; + } + }; + }, [table.isRowExpanded, userSettings]); +} + +/** + * Display a sub-table of the BOM, for displaying sub-assemblies within the main BOM table. + */ +export default function BomSubassemblyTable({ + partId +}: { + partId: number; +}) { + const table = useTable('bom-subassembly'); + + const rowExpansion = subassemblyRowExpansion({ table: table }); + + const userSettings = useUserSettingsState(); + + const tableColumns: TableColumn[] = useMemo(() => { + const allowExpansion = userSettings.isSet('SHOW_BOM_SUBASSEMBLY_LEVELS'); + + return [ + { + accessor: 'sub_part', + render: (record: any) => { + return ( + + {record.sub_part_detail?.assembly && allowExpansion && ( + + )} + + + ); + } + }, + IPNColumn({ + accessor: 'sub_part_detail.IPN' + }), + ReferenceColumn({}), + { + accessor: 'quantity' + } + ]; + }, [table.isRowExpanded, userSettings]); + + return ( + + + + + {}, + rowExpansion: rowExpansion, + enableSearch: false, + enableFilters: false, + enableColumnSwitching: false, + enableRefresh: false, + enableReports: false, + params: { + part: partId, + substitutes: false, + part_detail: true, + sub_part_detail: true + } + }} + /> + + + + ); +} diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index 4833218473..ec78463b81 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -40,6 +40,7 @@ import { useEditApiFormModal } from '../../hooks/UseForm'; import { useImporterState } from '../../states/ImporterState'; +import { useUserSettingsState } from '../../states/SettingsStates'; import { useUserState } from '../../states/UserState'; import { BooleanColumn, @@ -52,7 +53,9 @@ import { } from '../ColumnRenderers'; import { PartCategoryFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; +import RowExpansionIcon from '../RowExpansionIcon'; import { TableHoverCard } from '../TableHoverCard'; +import { subassemblyRowExpansion } from './BomSubassemblyTable'; // Calculate the total stock quantity available for a given BomItem function availableStockQuantity(record: any): number { @@ -88,7 +91,11 @@ export function BomTable({ const [isEditing, setIsEditing] = useState(false); + const userSettings = useUserSettingsState(); + const tableColumns: TableColumn[] = useMemo(() => { + const allowExpansion = userSettings.isSet('SHOW_BOM_SUBASSEMBLY_LEVELS'); + return [ { accessor: 'sub_part', @@ -117,10 +124,22 @@ export function BomTable({ ); } + const assembly: boolean = record.sub_part_detail?.assembly ?? false; + return ( part && ( } + value={ + + {assembly && !isEditing && allowExpansion && ( + + )} + + + } iconColor={record.validated ? undefined : 'red'} extra={extra} title={t`Part Information`} @@ -409,7 +428,7 @@ export function BomTable({ }, NoteColumn({}) ]; - }, [isEditing, partId, params]); + }, [table.isRowExpanded, isEditing, partId, params, userSettings]); const tableFilters: TableFilter[] = useMemo(() => { return [ @@ -535,7 +554,7 @@ export function BomTable({ table: table }); - const editSubstitues = useEditBomSubstitutesForm({ + const editSubstitutes = useEditBomSubstitutesForm({ bomItemId: selectedBomItem.pk, bomItem: selectedBomItem, onClose: () => { @@ -608,7 +627,7 @@ export function BomTable({ icon: , onClick: () => { setSelectedBomItem(record); - editSubstitues.open(); + editSubstitutes.open(); } }, RowDeleteAction({ @@ -669,13 +688,16 @@ export function BomTable({ ]; }, [isEditing, partLocked, user]); + // Row expansion (for displaying subassemblies) + const rowExpansion = subassemblyRowExpansion({ table: table }); + return ( <> {importBomItem.modal} {newBomItem.modal} {editBomItem.modal} {deleteBomItem.modal} - {editSubstitues.modal} + {editSubstitutes.modal} {partLocked && ( diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index d44d29b746..b70a561d09 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -179,6 +179,11 @@ test('Parts - BOM', async ({ browser }) => { // Move the mouse away await page.getByRole('link', { name: 'Bill of Materials' }).hover(); + // Test sub-assembly row expansion + await page.getByText('Widget Board (assembled)').click(); + await page.getByText('R_10R_0402_1%').waitFor(); + await page.getByText('MAX232IDR').waitFor(); + // Enable BOM editing await page.getByRole('button', { name: 'action-button-edit-bom' }).click(); await page