mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-06 09:43:38 +00:00
[feature] Bulk replace (#11861)
* Allow bulk replace of BOM items * Add "locked" filter for UsedIn table * Add playwright tests * docs * Bump API version * Update CHANGELOG * Update api_version.py --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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: (
|
||||
<Stack gap='xs'>
|
||||
<Alert
|
||||
color='orange'
|
||||
icon={<IconReplace />}
|
||||
title={t`Replace Component`}
|
||||
mb='md'
|
||||
>
|
||||
<Text>{t`This action cannot be easily undone, so please ensure you have selected the correct assemblies.`}</Text>
|
||||
</Alert>
|
||||
<Text>{t`The selected assemblies will be updated with the new component.`}</Text>
|
||||
{table.selectedRecords.map((record: any) => {
|
||||
return <RenderPartColumn part={record.part_detail} key={record.pk} />;
|
||||
})}
|
||||
<Divider />
|
||||
</Stack>
|
||||
),
|
||||
fields: {
|
||||
sub_part: {
|
||||
filters: {
|
||||
active: true,
|
||||
component: true
|
||||
},
|
||||
default: partId
|
||||
}
|
||||
},
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<ActionButton
|
||||
tooltip={t`Replace Component`}
|
||||
icon={<IconReplace />}
|
||||
color='blue'
|
||||
onClick={bulkReplace.open}
|
||||
hidden={!user.hasChangeRole(UserRoles.bom)}
|
||||
disabled={!table.selectedIds.length}
|
||||
/>
|
||||
];
|
||||
}, [user, table.selectedIds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{editBomItem.modal}
|
||||
{bulkReplace.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.bom_list)}
|
||||
tableState={table}
|
||||
@@ -147,10 +207,12 @@ export function UsedInTable({
|
||||
part_detail: true,
|
||||
sub_part_detail: true
|
||||
},
|
||||
enableSelection: user.hasChangeRole(UserRoles.bom),
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters,
|
||||
modelType: ModelType.part,
|
||||
modelField: 'part'
|
||||
modelField: 'part',
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user