diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 7444e5b670..8254d161d6 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -64,6 +64,13 @@ class InvenTreeSetting(models.Model): 'description': _('Regular expression pattern for matching Part IPN') }, + 'PART_ALLOW_DUPLICATE_IPN': { + 'name': _('Allow Duplicate IPN'), + 'description': _('Allow multiple parts to share the same IPN'), + 'default': True, + 'validator': bool, + }, + 'PART_COPY_BOM': { 'name': _('Copy Part BOM Data'), 'description': _('Copy BOM data by default when duplicating a part'), @@ -305,6 +312,10 @@ class InvenTreeSetting(models.Model): setting = InvenTreeSetting(key=key) else: return + + # Enforce standard boolean representation + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) setting.value = str(value) setting.save() @@ -317,6 +328,10 @@ class InvenTreeSetting(models.Model): def name(self): return InvenTreeSetting.get_setting_name(self.key) + @property + def default_value(self): + return InvenTreeSetting.get_default_value(self.key) + @property def description(self): return InvenTreeSetting.get_setting_description(self.key) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index dcf37fb98d..323049f164 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -70,3 +70,13 @@ class SettingsTest(TestCase): InvenTreeSetting.set_setting(key, value, self.user) self.assertEqual(value, InvenTreeSetting.get_setting(key)) + + # Any fields marked as 'boolean' must have a default value specified + setting = InvenTreeSetting.get_setting_object(key) + + if setting.is_bool(): + if setting.default_value in ['', None]: + raise ValueError(f'Default value for boolean setting {key} not provided') + + if setting.default_value not in [True, False]: + raise ValueError(f'Non-boolean default value specified for {key}') diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 54b67057e5..d6c536db59 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -529,6 +529,18 @@ class Part(MPTTModel): """ super().validate_unique(exclude) + # User can decide whether duplicate IPN (Internal Part Number) values are allowed + allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting('PART_ALLOW_DUPLICATE_IPN') + + if not allow_duplicate_ipn: + parts = Part.objects.filter(IPN__iexact=self.IPN) + parts = parts.exclude(pk=self.pk) + + if parts.exists(): + raise ValidationError({ + 'IPN': _('Duplicate IPN not allowed in part settings'), + }) + # Part name uniqueness should be case insensitive try: parts = Part.objects.exclude(id=self.id).filter( diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 44578331a9..677b159762 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -249,3 +249,28 @@ class PartSettingsTest(TestCase): self.assertEqual(part.trackable, val) Part.objects.filter(pk=part.pk).delete() + + def test_duplicate_ipn(self): + """ + Test the setting which controls duplicate IPN values + """ + + # Create a part + Part.objects.create(name='Hello', description='A thing', IPN='IPN123') + + # Attempt to create a duplicate item (should fail) + with self.assertRaises(ValidationError): + Part.objects.create(name='Hello', description='A thing', IPN='IPN123') + + # Attempt to create item with duplicate IPN (should be allowed by default) + Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='B') + + # And attempt again with the same values (should fail) + with self.assertRaises(ValidationError): + Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='B') + + # Now update the settings so duplicate IPN values are *not* allowed + InvenTreeSetting.set_setting('PART_ALLOW_DUPLICATE_IPN', False, self.user) + + with self.assertRaises(ValidationError): + Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='C') diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 2ec42dd2f3..4899ddee8d 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1247,12 +1247,21 @@ class StockItem(MPTTModel): @property def required_test_count(self): + """ + Return the number of 'required tests' for this StockItem + """ return self.part.getRequiredTests().count() def hasRequiredTests(self): + """ + Return True if there are any 'required tests' associated with this StockItem + """ return self.part.getRequiredTests().count() > 0 def passedAllRequiredTests(self): + """ + Returns True if this StockItem has passed all required tests + """ status = self.requiredTestStatus() diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index f6b5f3e4b0..a19ce83922 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -17,6 +17,8 @@
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} + {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} +