2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-12 11:38:47 +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:
Oliver
2026-05-24 07:41:52 +10:00
committed by GitHub
parent 8e7465dd24
commit 7d61203be8
12 changed files with 174 additions and 37 deletions
@@ -402,6 +402,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'choices': barcode_plugins,
'default': 'inventreebarcode',
},
'PART_ENABLE_LOCKING': {
'name': _('Part Locking'),
'description': _('Enable locking of parts to prevent modification'),
'validator': bool,
'default': True,
},
'PART_ENABLE_REVISION': {
'name': _('Part Revisions'),
'description': _('Enable revision field for Part'),
+6 -5
View File
@@ -570,13 +570,13 @@ class Part(
}
def check_parameter_delete(self, parameter):
"""Custom delete check for Paramteter instances associated with this Part."""
if self.locked:
"""Custom delete check for Parameter instances associated with this Part."""
if self.locked and get_global_setting('PART_ENABLE_LOCKING'):
raise ValidationError(_('Cannot delete parameters of a locked part'))
def check_parameter_save(self, parameter):
"""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'))
def delete(self, **kwargs):
@@ -587,7 +587,7 @@ class Part(
- The part is still active
- 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'))
if self.active:
@@ -4066,7 +4066,8 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
Raises:
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:
raise ValidationError(_('BOM item cannot be modified - assembly is locked'))
@@ -8,6 +8,7 @@ from django.test import TestCase
import build.models
import stock.models
from common.settings import set_global_setting
from .models import BomItem, BomItemSubstitute, Part
@@ -391,6 +392,44 @@ class BomItemTest(TestCase):
# Delete the new BOM item
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):
"""Test for caching of 'bom_validated' property."""
from part.tasks import validate_bom
+38
View File
@@ -6,6 +6,7 @@ from django.test import TestCase, TransactionTestCase
from django.urls import reverse
from common.models import InvenTreeSetting, Parameter, ParameterTemplate
from common.settings import set_global_setting
from InvenTree.unit_test import InvenTreeAPITestCase
from .models import Part, PartCategory, PartCategoryParameterTemplate
@@ -141,6 +142,43 @@ class TestParams(TestCase):
# And we can delete the parameter
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):
"""Test class for PartCategoryParameterTemplate model."""
+22
View File
@@ -353,6 +353,28 @@ class PartTest(TestCase):
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):
"""Test the 'revision' and 'revision_of' field."""
template = Part.objects.create(