Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0e5abfb
Support minimum column width
SchrodingersGat Aug 20, 2025
5503b7d
Adjust DescriptionColumn and StatusColumn
SchrodingersGat Aug 20, 2025
29e9b31
Column refactoring
SchrodingersGat Aug 20, 2025
d89a070
Refactor PartColumn
SchrodingersGat Aug 20, 2025
6857009
Refactor LineItemsProgerssColumn
SchrodingersGat Aug 20, 2025
a5ea349
Tweaks
SchrodingersGat Aug 20, 2025
99b272b
Merge branch 'master' of github.com:inventree/InvenTree
SchrodingersGat Aug 20, 2025
738a3d4
Merge branches 'master' and 'master' of github.com:inventree/InvenTree
SchrodingersGat Aug 21, 2025
15ddb21
Ensure "can_build" value is not negative
SchrodingersGat Aug 21, 2025
fe5c150
Render row expansion icon
SchrodingersGat Aug 21, 2025
04e811c
Add subassembly table for BOM
SchrodingersGat Aug 21, 2025
8d15641
Add controls for BOM editing
SchrodingersGat Aug 21, 2025
a26d679
Fix row click context
SchrodingersGat Aug 21, 2025
82716f3
Merge branch 'master' into bom-expand
SchrodingersGat Sep 3, 2025
69b33bb
Improve rendering for BOM sub-rows
SchrodingersGat Sep 3, 2025
5f4f392
Merge branch 'master' into bom-expand
SchrodingersGat Sep 3, 2025
7e9e3fb
Merge branch 'master' into bom-expand
SchrodingersGat Oct 2, 2025
447a3e8
Merge branch 'bom-expand' of github.com:SchrodingersGat/InvenTree int…
SchrodingersGat Oct 2, 2025
2e9d317
Merge commit 'd24ba7965c9e8d94d1eb638c0fbe5b67632180c6' into bom-expand
SchrodingersGat Feb 11, 2026
d896399
Merge commit 'fef0fdf99bb7193760dab8214d3c610bbe030616' into bom-expand
SchrodingersGat Apr 24, 2026
911ff21
Hide BOM actions unless editing
SchrodingersGat Apr 24, 2026
dae94ea
Disable row expansion for now
SchrodingersGat Apr 24, 2026
5a2b2fe
Revert gitleaks changes
SchrodingersGat Apr 24, 2026
6fc9bf2
Remove gitleaks tags
SchrodingersGat Apr 24, 2026
3231453
Remove dead code
SchrodingersGat Apr 24, 2026
5ddc0c6
Remove commented code
SchrodingersGat Apr 24, 2026
91bff37
Adjust playwright tests
SchrodingersGat Apr 25, 2026
2b2ffd0
Merge branch 'master' into bom-expand
SchrodingersGat Apr 26, 2026
39dd40f
Update docs
SchrodingersGat Apr 26, 2026
d06127b
Further playwright fixes
SchrodingersGat Apr 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/docs/manufacturing/bom.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ Note that inherited BOM Line Items only flow "downwards" in the variant inherita
!!! info "Editing Inherited Items"
When editing an inherited BOM Line Item for a template part, the changes are automatically reflected in the BOM of any variant parts.

## BOM Creation
## BOM Editing

BOMs can be created manually, by adjusting individual line items, or by uploading (importing) an existing BOM file.
Bills of Material (BOMs) can be created manually, by adjusting individual line items, or by uploading (importing) an existing BOM file.

### Editing Mode

By default, the BOM is displayed in "view" mode. To edit the BOM, click on the {{ icon("edit", color="blue", title="Edit") }} icon at the top of the BOM panel. This will enable editing mode, which allows you to add, adjust or delete BOM line items.

!!! warning "Permissions"
Only users with the appropriate permissions can edit BOMs. If you do not have permission to edit the BOM, the "Edit" icon will not be visible.

### Importing a BOM

Expand Down
94 changes: 61 additions & 33 deletions src/frontend/src/tables/bom/BomTable.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import { t } from '@lingui/core/macro';
import { Alert, Group, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import {
IconArrowRight,
IconCircleCheck,
IconEdit,
IconFileUpload,
IconLock,
IconPlus,
IconSwitch3
} from '@tabler/icons-react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { ActionButton } from '@lib/components/ActionButton';
import {
type RowAction,
RowDeleteAction,
Expand All @@ -12,20 +28,6 @@ import { navigateToLink } from '@lib/functions/Navigation';
import useTable from '@lib/hooks/UseTable';
import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { Alert, Group, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import {
IconArrowRight,
IconCircleCheck,
IconFileUpload,
IconLock,
IconPlus,
IconSwitch3
} from '@tabler/icons-react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Thumbnail } from '../../components/images/Thumbnail';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import { RenderPart } from '../../components/render/Part';
import { useApi } from '../../contexts/ApiContext';
Expand All @@ -45,7 +47,8 @@ import {
DescriptionColumn,
IPNColumn,
NoteColumn,
ReferenceColumn
ReferenceColumn,
RenderPartColumn
} from '../ColumnRenderers';
import { PartCategoryFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
Expand Down Expand Up @@ -80,27 +83,35 @@ export function BomTable({
const user = useUserState();
const table = useTable('bom');
const navigate = useNavigate();

const openImporter = useImporterState((state) => state.openImporter);

const [isEditing, setIsEditing] = useState<boolean>(false);

const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'sub_part',
switchable: false,
sortable: true,
minWidth: 250,
render: (record: any) => {
const part = record.sub_part_detail;

const extra = [];

if (record.part != partId) {
if (partId && record.part != partId) {
extra.push(
<Text key='different-parent'>{t`This BOM item is defined for a different parent`}</Text>
<Text
key='different-parent'
size='sm'
>{t`This BOM item is defined for a different parent`}</Text>
);
}

if (!record.validated) {
extra.push(
<Text key='not-validated' c='red'>
<Text key='not-validated' c='red' size='sm'>
{t`This BOM item has not been validated`}
</Text>
);
Expand All @@ -109,13 +120,7 @@ export function BomTable({
return (
part && (
<TableHoverCard
value={
<Thumbnail
src={part.thumbnail || part.image}
alt={part.description}
text={part.full_name}
/>
}
value={<RenderPartColumn part={part} />}
iconColor={record.validated ? undefined : 'red'}
extra={extra}
title={t`Part Information`}
Expand Down Expand Up @@ -376,7 +381,8 @@ export function BomTable({
return '-';
}

const can_build = Math.trunc(record.can_build);
const can_build = Math.max(0, Math.trunc(record.can_build));

const value = (
<Text
fs={record.consumable && 'italic'}
Expand All @@ -403,7 +409,7 @@ export function BomTable({
},
NoteColumn({})
];
}, [partId, params]);
}, [isEditing, partId, params]);

const tableFilters: TableFilter[] = useMemo(() => {
return [
Expand Down Expand Up @@ -614,7 +620,7 @@ export function BomTable({
})
];
},
[partId, partLocked, user]
[isEditing, partId, partLocked, user]
);

const tableActions = useMemo(() => {
Expand All @@ -624,7 +630,7 @@ export function BomTable({
tooltip={t`Add BOM Items`}
position='bottom-start'
icon={<IconPlus />}
hidden={partLocked || !user.hasAddRole(UserRoles.part)}
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.part)}
actions={[
{
name: t`Add BOM Item`,
Expand All @@ -639,9 +645,29 @@ export function BomTable({
onClick: () => importBomItem.open()
}
]}
/>,
<ActionButton
key='edit-bom'
hidden={partLocked || !user.hasChangeRole(UserRoles.part) || isEditing}
tooltip={t`Edit BOM`}
icon={<IconEdit />}
onClick={() => {
setIsEditing(true);
}}
/>,
<ActionButton
key='finish-editing'
hidden={!isEditing}
color='green'
tooltip={t`Finish Editing BOM`}
icon={<IconCircleCheck />}
onClick={() => {
setIsEditing(false);
table.refreshTable();
}}
/>
];
}, [partLocked, user]);
}, [isEditing, partLocked, user]);

return (
<>
Expand Down Expand Up @@ -678,9 +704,11 @@ export function BomTable({
tableFilters: tableFilters,
modelType: ModelType.part,
modelField: 'sub_part',
rowActions: rowActions,
enableSelection: !partLocked,
enableBulkDelete: !partLocked && user.hasDeleteRole(UserRoles.part),
onCellClick: () => {},
rowActions: isEditing ? rowActions : undefined,
enableSelection: isEditing && !partLocked,
enableBulkDelete:
isEditing && !partLocked && user.hasDeleteRole(UserRoles.part),
enableDownload: true
}}
/>
Expand Down
35 changes: 30 additions & 5 deletions src/frontend/tests/pages/pui_part.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { test } from '../baseFixtures';
import {
clearTableFilters,
Expand Down Expand Up @@ -179,10 +179,15 @@
// Move the mouse away
await page.getByRole('link', { name: 'Bill of Materials' }).hover();

const cell = await page.getByRole('cell', {
name: 'Small plastic enclosure, black',
exact: true
});
// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();

const cell = await page
.getByRole('cell', { name: 'Thumbnail 1551ABK' })
.first();

await clickOnRowMenu(cell);

Expand All @@ -202,6 +207,12 @@

await page.getByRole('button', { name: 'Add Substitute' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();

// Finish editing the BOM
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.click();
await page.getByRole('button', { name: 'action-button-edit-bom' }).waitFor();
});

/**
Expand All @@ -211,8 +222,15 @@
test('Parts - BOM Validation', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/107/bom' });

// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();

// Edit line item, to ensure BOM is not valid
const cell = await page.getByRole('cell', { name: 'Red paint Red Paint' });
const cell = await page.getByRole('cell', { name: 'Thumbnail Red Paint' });

await clickOnRowMenu(cell);
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();

Expand Down Expand Up @@ -288,6 +306,13 @@
const page = await doCachedLogin(browser, { url: 'part/104/bom' });

await loadTab(page, 'Bill of Materials');

// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();

await page
.getByRole('button', { name: 'action-menu-add-bom-items' })
.waitFor();
Expand Down Expand Up @@ -939,7 +964,7 @@
.getByRole('textbox', { name: 'textbox-search-for-part' })
.press('Enter');

await page.getByText('Bolt M5x5mm Steel').waitFor();

Check failure on line 967 in src/frontend/tests/pages/pui_part.spec.ts

View workflow job for this annotation

GitHub Actions / Tests - Web UI

[chromium] › tests/pages/pui_part.spec.ts:919:1 › Parts - Import supplier part

1) [chromium] › tests/pages/pui_part.spec.ts:919:1 › Parts - Import supplier part ──────────────── Error: locator.waitFor: Test ended. Call log: - waiting for getByText('Bolt M5x5mm Steel') to be visible 965 | .press('Enter'); 966 | > 967 | await page.getByText('Bolt M5x5mm Steel').waitFor(); | ^ 968 | await page 969 | .getByRole('button', { name: 'action-button-import-part-BOLT-Steel-M5-5' }) 970 | .click(); at /home/runner/work/InvenTree/InvenTree/src/frontend/tests/pages/pui_part.spec.ts:967:45
await page
.getByRole('button', { name: 'action-button-import-part-BOLT-Steel-M5-5' })
.click();
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/tests/pui_importing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ test('Importing - BOM', async ({ browser }) => {
url: 'part/109/bom'
});

// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();

// Open the BOM importer wizard
await page.getByRole('button', { name: 'action-menu-add-bom-items' }).click();

Expand Down
1 change: 1 addition & 0 deletions src/performance/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
server = os.environ.get('INVENTREE_PYTHON_TEST_SERVER', 'http://127.0.0.1:12345')
user = os.environ.get('INVENTREE_PYTHON_TEST_USERNAME', 'testuser')
pwd = os.environ.get('INVENTREE_PYTHON_TEST_PASSWORD', 'testpassword')

api_client = InvenTreeAPI(
server,
username=user,
Expand Down
Loading