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