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:
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user