mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-28 13:54:25 +00:00
[UI] BOM row expand (#11809)
* Add subassembly expansion Co-authored-by: Copilot <copilot@github.com> * Enable multi-level subassembly display Co-authored-by: Copilot <copilot@github.com> * Adjust padding * Adds user setting to enable / disable subassembly view Co-authored-by: Copilot <copilot@github.com> * Add icon * Additional playwright tests Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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.
|
- [#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
|
- [#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.
|
- [#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.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ The *Display Settings* screen shows general display configuration options:
|
|||||||
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
||||||
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
||||||
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
||||||
|
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
||||||
{{ usersetting("ENABLE_LAST_BREADCRUMB") }}
|
{{ usersetting("ENABLE_LAST_BREADCRUMB") }}
|
||||||
{{ usersetting("SHOW_FULL_LOCATION_IN_TABLES") }}
|
{{ usersetting("SHOW_FULL_LOCATION_IN_TABLES") }}
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'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': {
|
'NOTIFICATION_ERROR_REPORT': {
|
||||||
'name': _('Receive error reports'),
|
'name': _('Receive error reports'),
|
||||||
'description': _('Receive notifications for system errors'),
|
'description': _('Receive notifications for system errors'),
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ export default function UserSettings() {
|
|||||||
'DISPLAY_STOCKTAKE_TAB',
|
'DISPLAY_STOCKTAKE_TAB',
|
||||||
'ENABLE_LAST_BREADCRUMB',
|
'ENABLE_LAST_BREADCRUMB',
|
||||||
'SHOW_FULL_LOCATION_IN_TABLES',
|
'SHOW_FULL_LOCATION_IN_TABLES',
|
||||||
'SHOW_FULL_CATEGORY_IN_TABLES'
|
'SHOW_FULL_CATEGORY_IN_TABLES',
|
||||||
|
'SHOW_BOM_SUBASSEMBLY_LEVELS'
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 <BomSubassemblyTable partId={record.sub_part} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [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 (
|
||||||
|
<Group gap='xs' justify='left' p={0}>
|
||||||
|
{record.sub_part_detail?.assembly && allowExpansion && (
|
||||||
|
<RowExpansionIcon
|
||||||
|
enabled
|
||||||
|
expanded={table.isRowExpanded(record.pk)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<RenderPartColumn part={record.sub_part_detail} />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IPNColumn({
|
||||||
|
accessor: 'sub_part_detail.IPN'
|
||||||
|
}),
|
||||||
|
ReferenceColumn({}),
|
||||||
|
{
|
||||||
|
accessor: 'quantity'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [table.isRowExpanded, userSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper p={'xs'}>
|
||||||
|
<Group gap='xs' align='top' wrap={'nowrap'}>
|
||||||
|
<IconCornerLeftUp />
|
||||||
|
<Expand>
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiEndpoints.bom_list)}
|
||||||
|
tableState={table}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
modelType: ModelType.part,
|
||||||
|
modelField: 'sub_part',
|
||||||
|
onCellClick: () => {},
|
||||||
|
rowExpansion: rowExpansion,
|
||||||
|
enableSearch: false,
|
||||||
|
enableFilters: false,
|
||||||
|
enableColumnSwitching: false,
|
||||||
|
enableRefresh: false,
|
||||||
|
enableReports: false,
|
||||||
|
params: {
|
||||||
|
part: partId,
|
||||||
|
substitutes: false,
|
||||||
|
part_detail: true,
|
||||||
|
sub_part_detail: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Expand>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
import { useImporterState } from '../../states/ImporterState';
|
import { useImporterState } from '../../states/ImporterState';
|
||||||
|
import { useUserSettingsState } from '../../states/SettingsStates';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
BooleanColumn,
|
BooleanColumn,
|
||||||
@@ -52,7 +53,9 @@ import {
|
|||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { PartCategoryFilter } from '../Filter';
|
import { PartCategoryFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
import RowExpansionIcon from '../RowExpansionIcon';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
import { subassemblyRowExpansion } from './BomSubassemblyTable';
|
||||||
|
|
||||||
// Calculate the total stock quantity available for a given BomItem
|
// Calculate the total stock quantity available for a given BomItem
|
||||||
function availableStockQuantity(record: any): number {
|
function availableStockQuantity(record: any): number {
|
||||||
@@ -88,7 +91,11 @@ export function BomTable({
|
|||||||
|
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const userSettings = useUserSettingsState();
|
||||||
|
|
||||||
const tableColumns: TableColumn[] = useMemo(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
const allowExpansion = userSettings.isSet('SHOW_BOM_SUBASSEMBLY_LEVELS');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessor: 'sub_part',
|
accessor: 'sub_part',
|
||||||
@@ -117,10 +124,22 @@ export function BomTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assembly: boolean = record.sub_part_detail?.assembly ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
part && (
|
part && (
|
||||||
<TableHoverCard
|
<TableHoverCard
|
||||||
value={<RenderPartColumn part={part} />}
|
value={
|
||||||
|
<Group gap='xs' justify='left'>
|
||||||
|
{assembly && !isEditing && allowExpansion && (
|
||||||
|
<RowExpansionIcon
|
||||||
|
enabled
|
||||||
|
expanded={table.isRowExpanded(record.pk)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<RenderPartColumn part={part} />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
iconColor={record.validated ? undefined : 'red'}
|
iconColor={record.validated ? undefined : 'red'}
|
||||||
extra={extra}
|
extra={extra}
|
||||||
title={t`Part Information`}
|
title={t`Part Information`}
|
||||||
@@ -409,7 +428,7 @@ export function BomTable({
|
|||||||
},
|
},
|
||||||
NoteColumn({})
|
NoteColumn({})
|
||||||
];
|
];
|
||||||
}, [isEditing, partId, params]);
|
}, [table.isRowExpanded, isEditing, partId, params, userSettings]);
|
||||||
|
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@@ -535,7 +554,7 @@ export function BomTable({
|
|||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
|
||||||
const editSubstitues = useEditBomSubstitutesForm({
|
const editSubstitutes = useEditBomSubstitutesForm({
|
||||||
bomItemId: selectedBomItem.pk,
|
bomItemId: selectedBomItem.pk,
|
||||||
bomItem: selectedBomItem,
|
bomItem: selectedBomItem,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
@@ -608,7 +627,7 @@ export function BomTable({
|
|||||||
icon: <IconSwitch3 />,
|
icon: <IconSwitch3 />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedBomItem(record);
|
setSelectedBomItem(record);
|
||||||
editSubstitues.open();
|
editSubstitutes.open();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RowDeleteAction({
|
RowDeleteAction({
|
||||||
@@ -669,13 +688,16 @@ export function BomTable({
|
|||||||
];
|
];
|
||||||
}, [isEditing, partLocked, user]);
|
}, [isEditing, partLocked, user]);
|
||||||
|
|
||||||
|
// Row expansion (for displaying subassemblies)
|
||||||
|
const rowExpansion = subassemblyRowExpansion({ table: table });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{importBomItem.modal}
|
{importBomItem.modal}
|
||||||
{newBomItem.modal}
|
{newBomItem.modal}
|
||||||
{editBomItem.modal}
|
{editBomItem.modal}
|
||||||
{deleteBomItem.modal}
|
{deleteBomItem.modal}
|
||||||
{editSubstitues.modal}
|
{editSubstitutes.modal}
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
{partLocked && (
|
{partLocked && (
|
||||||
<Alert
|
<Alert
|
||||||
@@ -692,6 +714,7 @@ export function BomTable({
|
|||||||
tableState={table}
|
tableState={table}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
props={{
|
props={{
|
||||||
|
minHeight: 400,
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
part: partId,
|
part: partId,
|
||||||
@@ -709,7 +732,8 @@ export function BomTable({
|
|||||||
enableSelection: isEditing && !partLocked,
|
enableSelection: isEditing && !partLocked,
|
||||||
enableBulkDelete:
|
enableBulkDelete:
|
||||||
isEditing && !partLocked && user.hasDeleteRole(UserRoles.part),
|
isEditing && !partLocked && user.hasDeleteRole(UserRoles.part),
|
||||||
enableDownload: true
|
enableDownload: true,
|
||||||
|
rowExpansion: isEditing ? undefined : rowExpansion
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ test('Parts - BOM', async ({ browser }) => {
|
|||||||
// Move the mouse away
|
// Move the mouse away
|
||||||
await page.getByRole('link', { name: 'Bill of Materials' }).hover();
|
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
|
// Enable BOM editing
|
||||||
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
|
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
|
||||||
await page
|
await page
|
||||||
|
|||||||
Reference in New Issue
Block a user