mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-28 03:49:20 +00:00
Global part lock (#11995)
* Add new global setting * Update lock checks * Bug fix for task comparison * Update user interface * Hide locked field if locking not enabled * Update existing unit tests * Update docs
This commit is contained in:
@@ -96,6 +96,8 @@ Parts can be locked to prevent them from being modified. This is useful for part
|
|||||||
- BOM items cannot be created, edited, or deleted when they are part of a locked assembly
|
- BOM items cannot be created, edited, or deleted when they are part of a locked assembly
|
||||||
- Parameters linked to a locked part cannot be created, edited or deleted
|
- Parameters linked to a locked part cannot be created, edited or deleted
|
||||||
|
|
||||||
|
The part locking functionality can be enabled or disabled globally via the [Part Locking](../settings/global.md#parts) system setting (`PART_ENABLE_LOCKING`). When disabled, the locked state of a part is ignored and all operations are permitted.
|
||||||
|
|
||||||
## Active Parts
|
## Active Parts
|
||||||
|
|
||||||
By default, all parts are *Active*. Marking a part as inactive means it is not available for many actions, but the part remains in the database. If a part becomes obsolete, it is recommended that it is marked as inactive, rather than deleting it from the database.
|
By default, all parts are *Active*. Marking a part as inactive means it is not available for many actions, but the part remains in the database. If a part becomes obsolete, it is recommended that it is marked as inactive, rather than deleting it from the database.
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ Configuration of label printing:
|
|||||||
{{ globalsetting("PART_ALLOW_DUPLICATE_IPN") }}
|
{{ globalsetting("PART_ALLOW_DUPLICATE_IPN") }}
|
||||||
{{ globalsetting("PART_ALLOW_EDIT_IPN") }}
|
{{ globalsetting("PART_ALLOW_EDIT_IPN") }}
|
||||||
{{ globalsetting("PART_ALLOW_DELETE_FROM_ASSEMBLY") }}
|
{{ globalsetting("PART_ALLOW_DELETE_FROM_ASSEMBLY") }}
|
||||||
|
{{ globalsetting("PART_ENABLE_LOCKING") }}
|
||||||
{{ globalsetting("PART_ENABLE_REVISION") }}
|
{{ globalsetting("PART_ENABLE_REVISION") }}
|
||||||
{{ globalsetting("PART_REVISION_ASSEMBLY_ONLY") }}
|
{{ globalsetting("PART_REVISION_ASSEMBLY_ONLY") }}
|
||||||
{{ globalsetting("PART_NAME_FORMAT") }}
|
{{ globalsetting("PART_NAME_FORMAT") }}
|
||||||
|
|||||||
@@ -402,6 +402,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'choices': barcode_plugins,
|
'choices': barcode_plugins,
|
||||||
'default': 'inventreebarcode',
|
'default': 'inventreebarcode',
|
||||||
},
|
},
|
||||||
|
'PART_ENABLE_LOCKING': {
|
||||||
|
'name': _('Part Locking'),
|
||||||
|
'description': _('Enable locking of parts to prevent modification'),
|
||||||
|
'validator': bool,
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
'PART_ENABLE_REVISION': {
|
'PART_ENABLE_REVISION': {
|
||||||
'name': _('Part Revisions'),
|
'name': _('Part Revisions'),
|
||||||
'description': _('Enable revision field for Part'),
|
'description': _('Enable revision field for Part'),
|
||||||
|
|||||||
@@ -570,13 +570,13 @@ class Part(
|
|||||||
}
|
}
|
||||||
|
|
||||||
def check_parameter_delete(self, parameter):
|
def check_parameter_delete(self, parameter):
|
||||||
"""Custom delete check for Paramteter instances associated with this Part."""
|
"""Custom delete check for Parameter instances associated with this Part."""
|
||||||
if self.locked:
|
if self.locked and get_global_setting('PART_ENABLE_LOCKING'):
|
||||||
raise ValidationError(_('Cannot delete parameters of a locked part'))
|
raise ValidationError(_('Cannot delete parameters of a locked part'))
|
||||||
|
|
||||||
def check_parameter_save(self, parameter):
|
def check_parameter_save(self, parameter):
|
||||||
"""Custom save check for Parameter instances associated with this Part."""
|
"""Custom save check for Parameter instances associated with this Part."""
|
||||||
if self.locked:
|
if self.locked and get_global_setting('PART_ENABLE_LOCKING'):
|
||||||
raise ValidationError(_('Cannot modify parameters of a locked part'))
|
raise ValidationError(_('Cannot modify parameters of a locked part'))
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
@@ -587,7 +587,7 @@ class Part(
|
|||||||
- The part is still active
|
- The part is still active
|
||||||
- The part is used in a BOM for a different part.
|
- The part is used in a BOM for a different part.
|
||||||
"""
|
"""
|
||||||
if self.locked:
|
if self.locked and get_global_setting('PART_ENABLE_LOCKING'):
|
||||||
raise ValidationError(_('Cannot delete this part as it is locked'))
|
raise ValidationError(_('Cannot delete this part as it is locked'))
|
||||||
|
|
||||||
if self.active:
|
if self.active:
|
||||||
@@ -4066,7 +4066,8 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
|
|||||||
Raises:
|
Raises:
|
||||||
ValidationError: If the assembly is locked
|
ValidationError: If the assembly is locked
|
||||||
"""
|
"""
|
||||||
# TODO: Perhaps control this with a global setting?
|
if not get_global_setting('PART_ENABLE_LOCKING'):
|
||||||
|
return
|
||||||
|
|
||||||
if assembly.locked:
|
if assembly.locked:
|
||||||
raise ValidationError(_('BOM item cannot be modified - assembly is locked'))
|
raise ValidationError(_('BOM item cannot be modified - assembly is locked'))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.test import TestCase
|
|||||||
|
|
||||||
import build.models
|
import build.models
|
||||||
import stock.models
|
import stock.models
|
||||||
|
from common.settings import set_global_setting
|
||||||
|
|
||||||
from .models import BomItem, BomItemSubstitute, Part
|
from .models import BomItem, BomItemSubstitute, Part
|
||||||
|
|
||||||
@@ -391,6 +392,44 @@ class BomItemTest(TestCase):
|
|||||||
# Delete the new BOM item
|
# Delete the new BOM item
|
||||||
bom_item.delete()
|
bom_item.delete()
|
||||||
|
|
||||||
|
def test_locked_assembly_locking_disabled(self):
|
||||||
|
"""Test that a locked assembly is not enforced when PART_ENABLE_LOCKING is disabled."""
|
||||||
|
assembly = Part.objects.create(
|
||||||
|
name='Assembly3', description='An assembly part', assembly=True
|
||||||
|
)
|
||||||
|
sub_part = Part.objects.create(
|
||||||
|
name='SubPart2', description='A sub-part', component=True
|
||||||
|
)
|
||||||
|
|
||||||
|
bom_item = BomItem.objects.create(part=assembly, sub_part=sub_part, quantity=1)
|
||||||
|
|
||||||
|
assembly.locked = True
|
||||||
|
assembly.save()
|
||||||
|
|
||||||
|
# With locking enabled (default), editing is blocked
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
bom_item.quantity = 5
|
||||||
|
bom_item.save()
|
||||||
|
|
||||||
|
# Disable locking globally — all BOM operations should now be allowed
|
||||||
|
set_global_setting('PART_ENABLE_LOCKING', False)
|
||||||
|
|
||||||
|
bom_item.quantity = 5
|
||||||
|
bom_item.save()
|
||||||
|
|
||||||
|
BomItem.objects.create(part=assembly, sub_part=sub_part, quantity=2)
|
||||||
|
|
||||||
|
bom_item.delete()
|
||||||
|
|
||||||
|
# Re-enable for other tests
|
||||||
|
set_global_setting('PART_ENABLE_LOCKING', True)
|
||||||
|
|
||||||
|
# Confirm locking is enforced again
|
||||||
|
bom_item2 = BomItem.objects.get(part=assembly, sub_part=sub_part, quantity=2)
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
bom_item2.quantity = 99
|
||||||
|
bom_item2.save()
|
||||||
|
|
||||||
def test_bom_validated(self):
|
def test_bom_validated(self):
|
||||||
"""Test for caching of 'bom_validated' property."""
|
"""Test for caching of 'bom_validated' property."""
|
||||||
from part.tasks import validate_bom
|
from part.tasks import validate_bom
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.test import TestCase, TransactionTestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.models import InvenTreeSetting, Parameter, ParameterTemplate
|
from common.models import InvenTreeSetting, Parameter, ParameterTemplate
|
||||||
|
from common.settings import set_global_setting
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartCategoryParameterTemplate
|
from .models import Part, PartCategory, PartCategoryParameterTemplate
|
||||||
@@ -141,6 +142,43 @@ class TestParams(TestCase):
|
|||||||
# And we can delete the parameter
|
# And we can delete the parameter
|
||||||
parameter.delete()
|
parameter.delete()
|
||||||
|
|
||||||
|
def test_locked_part_locking_disabled(self):
|
||||||
|
"""Test that parameter restrictions are lifted when PART_ENABLE_LOCKING is disabled."""
|
||||||
|
part = Part.objects.create(
|
||||||
|
name='Test Part Lock Override',
|
||||||
|
description='Part for testing global locking override',
|
||||||
|
category=PartCategory.objects.first(),
|
||||||
|
)
|
||||||
|
|
||||||
|
parameter = Parameter.objects.create(
|
||||||
|
content_object=part, template=ParameterTemplate.objects.first(), data='100'
|
||||||
|
)
|
||||||
|
|
||||||
|
part.locked = True
|
||||||
|
part.save()
|
||||||
|
|
||||||
|
# With locking enabled (default), editing and deletion are blocked
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
parameter.data = '200'
|
||||||
|
parameter.save()
|
||||||
|
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
parameter.delete()
|
||||||
|
|
||||||
|
# Disable locking globally — parameter operations should now succeed
|
||||||
|
set_global_setting('PART_ENABLE_LOCKING', False)
|
||||||
|
|
||||||
|
parameter.data = '200'
|
||||||
|
parameter.save()
|
||||||
|
self.assertEqual(parameter.data, '200')
|
||||||
|
|
||||||
|
# Re-enable locking — editing should be blocked again
|
||||||
|
set_global_setting('PART_ENABLE_LOCKING', True)
|
||||||
|
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
parameter.data = '300'
|
||||||
|
parameter.save()
|
||||||
|
|
||||||
|
|
||||||
class TestCategoryTemplates(TransactionTestCase):
|
class TestCategoryTemplates(TransactionTestCase):
|
||||||
"""Test class for PartCategoryParameterTemplate model."""
|
"""Test class for PartCategoryParameterTemplate model."""
|
||||||
|
|||||||
@@ -353,6 +353,28 @@ class PartTest(TestCase):
|
|||||||
|
|
||||||
part.delete()
|
part.delete()
|
||||||
|
|
||||||
|
def test_delete_locking_disabled(self):
|
||||||
|
"""Test that a locked part can be deleted when PART_ENABLE_LOCKING is disabled."""
|
||||||
|
part = Part.objects.create(
|
||||||
|
name='Locked Part Test',
|
||||||
|
description='Part for testing locking override',
|
||||||
|
active=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
part.locked = True
|
||||||
|
part.save()
|
||||||
|
|
||||||
|
# With locking enabled (default), deletion of a locked part raises an error
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
part.delete()
|
||||||
|
|
||||||
|
# Disable locking globally — locked part should now be deletable
|
||||||
|
set_global_setting('PART_ENABLE_LOCKING', False)
|
||||||
|
part.delete()
|
||||||
|
|
||||||
|
# Re-enable for other tests
|
||||||
|
set_global_setting('PART_ENABLE_LOCKING', True)
|
||||||
|
|
||||||
def test_revisions(self):
|
def test_revisions(self):
|
||||||
"""Test the 'revision' and 'revision_of' field."""
|
"""Test the 'revision' and 'revision_of' field."""
|
||||||
template = Part.objects.create(
|
template = Part.objects.create(
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ export function usePartFields({
|
|||||||
delete fields['default_expiry'];
|
delete fields['default_expiry'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove "locked" field if locking not enabled
|
||||||
|
if (!globalSettings.isSet('PART_ENABLE_LOCKING')) {
|
||||||
|
delete fields['locked'];
|
||||||
|
}
|
||||||
|
|
||||||
if (create) {
|
if (create) {
|
||||||
delete fields['starred'];
|
delete fields['starred'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export default function SystemSettings() {
|
|||||||
'PART_ALLOW_DUPLICATE_IPN',
|
'PART_ALLOW_DUPLICATE_IPN',
|
||||||
'PART_ALLOW_EDIT_IPN',
|
'PART_ALLOW_EDIT_IPN',
|
||||||
'PART_ALLOW_DELETE_FROM_ASSEMBLY',
|
'PART_ALLOW_DELETE_FROM_ASSEMBLY',
|
||||||
|
'PART_ENABLE_LOCKING',
|
||||||
'PART_ENABLE_REVISION',
|
'PART_ENABLE_REVISION',
|
||||||
'PART_REVISION_ASSEMBLY_ONLY',
|
'PART_REVISION_ASSEMBLY_ONLY',
|
||||||
'PART_SHOW_RELATED',
|
'PART_SHOW_RELATED',
|
||||||
|
|||||||
@@ -202,6 +202,11 @@ export default function PartDetail() {
|
|||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lockingEnabled = useMemo(
|
||||||
|
() => globalSettings.isSet('PART_ENABLE_LOCKING'),
|
||||||
|
[globalSettings]
|
||||||
|
);
|
||||||
|
|
||||||
const revisionsEnabled = useMemo(
|
const revisionsEnabled = useMemo(
|
||||||
() => globalSettings.isSet('PART_ENABLE_REVISION'),
|
() => globalSettings.isSet('PART_ENABLE_REVISION'),
|
||||||
[globalSettings]
|
[globalSettings]
|
||||||
@@ -808,7 +813,12 @@ export default function PartDetail() {
|
|||||||
icon: <IconTestPipe />,
|
icon: <IconTestPipe />,
|
||||||
hidden: !part.testable,
|
hidden: !part.testable,
|
||||||
content: part?.pk ? (
|
content: part?.pk ? (
|
||||||
<PartTestTemplateTable partId={part?.pk} partLocked={part.locked} />
|
<PartTestTemplateTable
|
||||||
|
partId={part?.pk}
|
||||||
|
partLocked={
|
||||||
|
globalSettings.isSet('PART_ENABLE_LOCKING') && part?.locked
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
@@ -836,7 +846,7 @@ export default function PartDetail() {
|
|||||||
icon: <IconListDetails />,
|
icon: <IconListDetails />,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
{part.locked && (
|
{lockingEnabled && part.locked && (
|
||||||
<Alert
|
<Alert
|
||||||
title={t`Part is Locked`}
|
title={t`Part is Locked`}
|
||||||
color='orange'
|
color='orange'
|
||||||
@@ -849,7 +859,7 @@ export default function PartDetail() {
|
|||||||
<ParameterTable
|
<ParameterTable
|
||||||
modelType={ModelType.part}
|
modelType={ModelType.part}
|
||||||
modelId={part?.pk}
|
modelId={part?.pk}
|
||||||
allowEdit={part?.locked != true}
|
allowEdit={!lockingEnabled || part?.locked != true}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -1149,20 +1159,22 @@ export default function PartDetail() {
|
|||||||
<PageDetail
|
<PageDetail
|
||||||
title={`${t`Part`}: ${part.full_name}`}
|
title={`${t`Part`}: ${part.full_name}`}
|
||||||
icon={
|
icon={
|
||||||
<ActionIcon
|
lockingEnabled ? (
|
||||||
aria-label='part-lock-icon'
|
<ActionIcon
|
||||||
variant='transparent'
|
aria-label='part-lock-icon'
|
||||||
disabled={!user.hasChangeRole(UserRoles.part)}
|
variant='transparent'
|
||||||
onClick={() => {
|
disabled={!user.hasChangeRole(UserRoles.part)}
|
||||||
api
|
onClick={() => {
|
||||||
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
|
api
|
||||||
locked: !part.locked
|
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
|
||||||
})
|
locked: !part.locked
|
||||||
.then(refreshInstance);
|
})
|
||||||
}}
|
.then(refreshInstance);
|
||||||
>
|
}}
|
||||||
{part?.locked ? <IconLock /> : <IconLockOpen />}
|
>
|
||||||
</ActionIcon>
|
{part?.locked ? <IconLock /> : <IconLockOpen />}
|
||||||
|
</ActionIcon>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
subtitle={part.description}
|
subtitle={part.description}
|
||||||
imageUrl={part.image}
|
imageUrl={part.image}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export function RenderPartColumn({
|
|||||||
part: any;
|
part: any;
|
||||||
full_name?: boolean;
|
full_name?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const globalSettings = useGlobalSettingsState.getState();
|
||||||
|
|
||||||
if (!part) {
|
if (!part) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ export function RenderPartColumn({
|
|||||||
<IconExclamationCircle color='red' size={16} />
|
<IconExclamationCircle color='red' size={16} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{part?.locked && (
|
{globalSettings.isSet('PART_ENABLE_LOCKING') && part?.locked && (
|
||||||
<Tooltip label={t`Part is Locked`}>
|
<Tooltip label={t`Part is Locked`}>
|
||||||
<IconLock size={16} />
|
<IconLock size={16} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ 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 {
|
||||||
|
useGlobalSettingsState,
|
||||||
|
useUserSettingsState
|
||||||
|
} from '../../states/SettingsStates';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
BooleanColumn,
|
BooleanColumn,
|
||||||
@@ -91,6 +94,13 @@ export function BomTable({
|
|||||||
|
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
|
const isLocked = useMemo(
|
||||||
|
() => globalSettings.isSet('PART_ENABLE_LOCKING') && (partLocked ?? false),
|
||||||
|
[globalSettings, partLocked]
|
||||||
|
);
|
||||||
|
|
||||||
const userSettings = useUserSettingsState();
|
const userSettings = useUserSettingsState();
|
||||||
|
|
||||||
const tableColumns: TableColumn[] = useMemo(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
@@ -605,16 +615,14 @@ export function BomTable({
|
|||||||
title: t`Validate BOM Line`,
|
title: t`Validate BOM Line`,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
hidden:
|
hidden:
|
||||||
partLocked ||
|
isLocked || record.validated || !user.hasChangeRole(UserRoles.bom),
|
||||||
record.validated ||
|
|
||||||
!user.hasChangeRole(UserRoles.bom),
|
|
||||||
icon: <IconCircleCheck />,
|
icon: <IconCircleCheck />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
validateBomItem(record);
|
validateBomItem(record);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RowEditAction({
|
RowEditAction({
|
||||||
hidden: partLocked || !user.hasChangeRole(UserRoles.bom),
|
hidden: isLocked || !user.hasChangeRole(UserRoles.bom),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedBomItem(record);
|
setSelectedBomItem(record);
|
||||||
editBomItem.open();
|
editBomItem.open();
|
||||||
@@ -623,7 +631,7 @@ export function BomTable({
|
|||||||
{
|
{
|
||||||
title: t`Edit Substitutes`,
|
title: t`Edit Substitutes`,
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
hidden: partLocked || !user.hasAddRole(UserRoles.bom),
|
hidden: isLocked || !user.hasAddRole(UserRoles.bom),
|
||||||
icon: <IconSwitch3 />,
|
icon: <IconSwitch3 />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedBomItem(record);
|
setSelectedBomItem(record);
|
||||||
@@ -631,7 +639,7 @@ export function BomTable({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
RowDeleteAction({
|
RowDeleteAction({
|
||||||
hidden: partLocked || !user.hasDeleteRole(UserRoles.bom),
|
hidden: isLocked || !user.hasDeleteRole(UserRoles.bom),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedBomItem(record);
|
setSelectedBomItem(record);
|
||||||
deleteBomItem.open();
|
deleteBomItem.open();
|
||||||
@@ -639,7 +647,7 @@ export function BomTable({
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
[isEditing, partId, partLocked, user]
|
[isEditing, partId, isLocked, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
@@ -649,7 +657,7 @@ export function BomTable({
|
|||||||
tooltip={t`Add BOM Items`}
|
tooltip={t`Add BOM Items`}
|
||||||
position='bottom-start'
|
position='bottom-start'
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.bom)}
|
hidden={!isEditing || isLocked || !user.hasAddRole(UserRoles.bom)}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
name: t`Add BOM Item`,
|
name: t`Add BOM Item`,
|
||||||
@@ -667,7 +675,7 @@ export function BomTable({
|
|||||||
/>,
|
/>,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
key='edit-bom'
|
key='edit-bom'
|
||||||
hidden={partLocked || !user.hasChangeRole(UserRoles.bom) || isEditing}
|
hidden={isLocked || !user.hasChangeRole(UserRoles.bom) || isEditing}
|
||||||
tooltip={t`Edit BOM`}
|
tooltip={t`Edit BOM`}
|
||||||
icon={<IconEdit />}
|
icon={<IconEdit />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -686,7 +694,7 @@ export function BomTable({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [isEditing, partLocked, user]);
|
}, [isEditing, isLocked, user]);
|
||||||
|
|
||||||
// Row expansion (for displaying subassemblies)
|
// Row expansion (for displaying subassemblies)
|
||||||
const rowExpansion = subassemblyRowExpansion({ table: table });
|
const rowExpansion = subassemblyRowExpansion({ table: table });
|
||||||
@@ -699,7 +707,7 @@ export function BomTable({
|
|||||||
{deleteBomItem.modal}
|
{deleteBomItem.modal}
|
||||||
{editSubstitutes.modal}
|
{editSubstitutes.modal}
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
{partLocked && (
|
{isLocked && (
|
||||||
<Alert
|
<Alert
|
||||||
title={t`Part is Locked`}
|
title={t`Part is Locked`}
|
||||||
color='orange'
|
color='orange'
|
||||||
@@ -730,9 +738,9 @@ export function BomTable({
|
|||||||
modelField: 'sub_part',
|
modelField: 'sub_part',
|
||||||
onCellClick: () => {},
|
onCellClick: () => {},
|
||||||
rowActions: isEditing ? rowActions : undefined,
|
rowActions: isEditing ? rowActions : undefined,
|
||||||
enableSelection: isEditing && !partLocked,
|
enableSelection: isEditing && !isLocked,
|
||||||
enableBulkDelete:
|
enableBulkDelete:
|
||||||
isEditing && !partLocked && user.hasDeleteRole(UserRoles.bom),
|
isEditing && !isLocked && user.hasDeleteRole(UserRoles.bom),
|
||||||
enableDownload: true,
|
enableDownload: true,
|
||||||
rowExpansion: isEditing ? undefined : rowExpansion
|
rowExpansion: isEditing ? undefined : rowExpansion
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user