diff --git a/CHANGELOG.md b/CHANGELOG.md index 6988a362c5..457aa3295b 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 +- [#11861](https://github.com/inventree/InvenTree/pull/11861) adds support for bulk-replacing a component in multiple BOMs simultaneously - [#11853](https://github.com/inventree/InvenTree/pull/11853) adds BOM comparison functionality, allowing users to compare the BOM of one assembly with another assembly. - [#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. diff --git a/docs/docs/assets/images/build/replace_component.png b/docs/docs/assets/images/build/replace_component.png new file mode 100644 index 0000000000..bdf0cf6852 Binary files /dev/null and b/docs/docs/assets/images/build/replace_component.png differ diff --git a/docs/docs/manufacturing/bom.md b/docs/docs/manufacturing/bom.md index 87093f704e..b21c02b84a 100644 --- a/docs/docs/manufacturing/bom.md +++ b/docs/docs/manufacturing/bom.md @@ -192,3 +192,17 @@ When comparing BOMs from two different assemblies, the user can select from the | *Show common parts* | Display only the BOM line items which are common between the two assemblies. | In each case, any differences between the BOM line items are highlighted in red. + +## Replacing Components + +When a component is used in the BOM for multiple assemblies, it can be time consuming to update the BOM for each assembly when a change is required. InvenTree provides a "Replace Component" function which streamlines the process of replacing a component part with another part across multiple BOMs. + +To replace a component part within multiple assemblies: + +- Navigate to the [Used In](../part/views.md#used-in) tab of the component part detail page +- Select the assemblies you wish to update by ticking the checkbox next to each assembly +- Click on the {{ icon("replace", color="blue", title="Replace Component") }} icon to open the "Replace Component" dialog + +The following dialog will be displayed, which allows the user to select a new component part to replace the existing component part in the BOM of the selected assemblies: + +{{ image("build/replace_component.png", "Replace Component") }} diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 4c7c25654e..a326effd1e 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 482 +INVENTREE_API_VERSION = 483 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v483 -> 2026-05-04 : https://github.com/inventree/InvenTree/pull/11861 + - Enable bulk-update operations on the BomItem API endpoint, allowing multiple BOM items to be updated in a single API call + v482 -> 2026-03-15 : https://github.com/inventree/InvenTree/pull/11540 - Add id to the ordering fields of the Parts model diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 23e154f751..60daddad09 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -1290,6 +1290,10 @@ class BomFilter(FilterSet): label=_('Assembly part is testable'), field_name='part__testable' ) + part_locked = rest_filters.BooleanFilter( + label=_('Assembly part is locked'), field_name='part__locked' + ) + # Filters for linked 'sub_part' sub_part_active = rest_filters.BooleanFilter( label=_('Component part is active'), field_name='sub_part__active' @@ -1406,7 +1410,11 @@ class BomOutputOptions(OutputConfiguration): class BomList( - BomMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateDestroyAPIView + BomMixin, + BulkUpdateMixin, + DataExportViewMixin, + OutputOptionsMixin, + ListCreateDestroyAPIView, ): """API endpoint for accessing a list of BomItem objects. diff --git a/src/frontend/src/tables/bom/UsedInTable.tsx b/src/frontend/src/tables/bom/UsedInTable.tsx index 059e0dbc9a..034b05c8b8 100644 --- a/src/frontend/src/tables/bom/UsedInTable.tsx +++ b/src/frontend/src/tables/bom/UsedInTable.tsx @@ -1,23 +1,28 @@ import { t } from '@lingui/core/macro'; -import { Group, Text } from '@mantine/core'; +import { Alert, Divider, Group, Stack, Text } from '@mantine/core'; import { useCallback, useMemo, useState } from 'react'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; import useTable from '@lib/hooks/UseTable'; -import { RowEditAction, UserRoles } from '@lib/index'; +import { ActionButton, RowEditAction, UserRoles } from '@lib/index'; import type { TableFilter } from '@lib/types/Filters'; import type { RowAction, TableColumn } from '@lib/types/Tables'; +import { IconReplace } from '@tabler/icons-react'; import { formatDecimal } from '../../defaults/formatters'; import { bomItemFields } from '../../forms/BomForms'; -import { useEditApiFormModal } from '../../hooks/UseForm'; +import { + useBulkEditApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; import { useUserState } from '../../states/UserState'; import { DescriptionColumn, IPNColumn, PartColumn, - ReferenceColumn + ReferenceColumn, + RenderPartColumn } from '../ColumnRenderers'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -85,6 +90,11 @@ export function UsedInTable({ label: t`Inherited`, description: t`Show inherited items` }, + { + name: 'part_locked', + label: t`Locked`, + description: t`Show locked assemblies` + }, { name: 'optional', label: t`Optional`, @@ -122,7 +132,7 @@ export function UsedInTable({ return [ RowEditAction({ - hidden: locked || !user.hasChangeRole(UserRoles.part), + hidden: locked || !user.hasChangeRole(UserRoles.bom), onClick: () => { setSelectedBomItem(record); editBomItem.open(); @@ -133,9 +143,59 @@ export function UsedInTable({ [user] ); + const bulkReplaceParts = useMemo(() => {}, [table.selectedRecords]); + + const bulkReplace = useBulkEditApiFormModal({ + url: ApiEndpoints.bom_list, + items: table.selectedIds, + title: t`Replace Component`, + submitText: t`Replace`, + preFormContent: ( + + } + title={t`Replace Component`} + mb='md' + > + {t`This action cannot be easily undone, so please ensure you have selected the correct assemblies.`} + + {t`The selected assemblies will be updated with the new component.`} + {table.selectedRecords.map((record: any) => { + return ; + })} + + + ), + fields: { + sub_part: { + filters: { + active: true, + component: true + }, + default: partId + } + }, + onFormSuccess: table.refreshTable + }); + + const tableActions = useMemo(() => { + return [ + } + color='blue' + onClick={bulkReplace.open} + hidden={!user.hasChangeRole(UserRoles.bom)} + disabled={!table.selectedIds.length} + /> + ]; + }, [user, table.selectedIds]); + return ( <> {editBomItem.modal} + {bulkReplace.modal} diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index c7d9adacd6..4fd3df805c 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -305,6 +305,34 @@ test('Parts - BOM Comparison', async ({ browser }) => { await page.getByText('Blue Paint', { exact: true }).first().waitFor(); }); +test('Parts - Used In', async ({ browser }) => { + const page = await doCachedLogin(browser, { url: 'part/4/used_in' }); + + // Check for expected elements + await page.getByRole('button', { name: 'Assembly Not sorted' }).waitFor(); + await page.getByText('R33, R34, R35, R36').waitFor(); + + // Edit row + const cell = await page.getByRole('cell', { name: 'Thumbnail Test Board 1' }); + await cell.click({ button: 'right' }); + await page.getByRole('button', { name: 'Edit' }).first().click(); + await page.getByRole('textbox', { name: 'text-field-reference' }).waitFor(); + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Attempt to replace this part in multiple assemblies + await page.getByRole('checkbox', { name: 'Select all records' }).click(); + await page.getByRole('button', { name: 'action-button-replace-' }).click(); + await page.getByText('This action cannot be easily undone').waitFor(); + + // Submit the form - locked parts should throw an error + await page.getByRole('button', { name: 'Replace', exact: true }).click(); + + await page.getByText('Form Error').waitFor(); + await page + .getByText('BOM item cannot be modified - assembly is locked') + .waitFor(); +}); + test('Parts - Editing', async ({ browser }) => { const page = await doCachedLogin(browser, { url: 'part/104/details' });