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
|
||||
|
||||
- [#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.
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
} 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<boolean>(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 && (
|
||||
<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'}
|
||||
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: <IconSwitch3 />,
|
||||
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}
|
||||
<Stack gap='xs'>
|
||||
{partLocked && (
|
||||
<Alert
|
||||
@@ -692,6 +714,7 @@ export function BomTable({
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
minHeight: 400,
|
||||
params: {
|
||||
...params,
|
||||
part: partId,
|
||||
@@ -709,7 +732,8 @@ export function BomTable({
|
||||
enableSelection: isEditing && !partLocked,
|
||||
enableBulkDelete:
|
||||
isEditing && !partLocked && user.hasDeleteRole(UserRoles.part),
|
||||
enableDownload: true
|
||||
enableDownload: true,
|
||||
rowExpansion: isEditing ? undefined : rowExpansion
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user