diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 55e26db261f4..7aaa21c60963 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -1032,6 +1032,15 @@ def validate_unique(self, exclude=None): parts = Part.objects.filter(IPN__iexact=self.IPN) parts = parts.exclude(pk=self.pk) + # Parts in the same revision family share the same IPN by design, + # so exclude them from the duplicate check + if self.revision_of: + # Exclude the parent revision and all sibling revisions + parts = parts.exclude(pk=self.revision_of.pk) + parts = parts.exclude(revision_of=self.revision_of) + # Exclude any revisions of this part + parts = parts.exclude(revision_of=self) + if parts.exists(): raise ValidationError({ 'IPN': _('Duplicate IPN not allowed in part settings') diff --git a/src/backend/InvenTree/part/test_part.py b/src/backend/InvenTree/part/test_part.py index 05e58d225ee5..bc2c8f3283a8 100644 --- a/src/backend/InvenTree/part/test_part.py +++ b/src/backend/InvenTree/part/test_part.py @@ -827,6 +827,44 @@ def test_duplicate_ipn(self): Part.objects.create(name='abc', revision='5', description='A part', IPN=' ') Part.objects.create(name='abc', revision='6', description='A part', IPN=' ') + def test_duplicate_ipn_with_revisions(self): + """Revisions of the same part share the same IPN and must not trigger a duplicate IPN error. + + Regression test for https://github.com/inventree/InvenTree/issues/12017 + """ + set_global_setting('PART_ALLOW_DUPLICATE_IPN', False, self.user) + + # Create the base part (revision 4) + base = Part.objects.create( + name='Widget', + description='A widget', + IPN='AS0001234', + revision='4', + assembly=True, + ) + + # Create a revision of the base part with the same IPN — must not raise + revision = Part( + name='Widget', + description='A widget', + IPN='AS0001234', + revision='5', + revision_of=base, + assembly=True, + ) + revision.validate_unique() # should not raise + revision.save() + + # Saving the revision again (e.g. after unlocking) must also not raise + revision.validate_unique() + + # An unrelated part with the same IPN must still be rejected + with self.assertRaises(ValidationError): + unrelated = Part( + name='Other', description='Other part', IPN='AS0001234', revision='1' + ) + unrelated.validate_unique() + class PartSubscriptionTests(InvenTreeTestCase): """Unit tests for part 'subscription'."""