mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-10 15:36:17 +00:00
[bug] Fix part duplicate checks (#12574)
* [bug] Fix part duplicate checks * Additional unit tests
This commit is contained in:
@@ -1053,11 +1053,20 @@ class Part(
|
||||
raise ValidationError(_('Duplicate part revision already exists.'))
|
||||
|
||||
# Ensure unique across (Name, revision, IPN) (as specified)
|
||||
if (self.revision or self.IPN) and (
|
||||
Part.objects
|
||||
.exclude(pk=self.pk)
|
||||
.filter(name=self.name, revision=self.revision, IPN=self.IPN)
|
||||
.exists()
|
||||
# Note: the 'unique_part' database constraint only rejects a row when
|
||||
# *all three* fields are non-null (Postgres treats NULL as distinct from
|
||||
# NULL), so mirror that here rather than skipping whenever either field
|
||||
# is merely blank - an empty string ('') still collides with another
|
||||
# empty string at the database level, unlike None/NULL.
|
||||
if (
|
||||
self.IPN is not None
|
||||
and self.revision is not None
|
||||
and (
|
||||
Part.objects
|
||||
.exclude(pk=self.pk)
|
||||
.filter(name=self.name, revision=self.revision, IPN=self.IPN)
|
||||
.exists()
|
||||
)
|
||||
):
|
||||
raise ValidationError(
|
||||
_('Part with this Name, IPN and Revision already exists.')
|
||||
|
||||
@@ -1638,6 +1638,36 @@ class PartCreationTests(PartAPITestBase):
|
||||
self.assertFalse(response.data['active'])
|
||||
self.assertFalse(response.data['purchaseable'])
|
||||
|
||||
def test_create_duplicate_no_ipn_revision(self):
|
||||
"""Test that creating a duplicate part (same name, no IPN/revision) returns a 400.
|
||||
|
||||
Regression test for a bug where the duplicate-name check was skipped
|
||||
whenever IPN and revision were both blank, letting the request fall
|
||||
through to an unhandled database IntegrityError (500) instead of a
|
||||
proper validation error (400).
|
||||
"""
|
||||
url = reverse('api-part-list')
|
||||
|
||||
data = {
|
||||
'name': 'TEST1',
|
||||
'category': 1,
|
||||
'assembly': True,
|
||||
'component': True,
|
||||
'consumable': False,
|
||||
'is_template': False,
|
||||
'purchaseable': False,
|
||||
'salable': False,
|
||||
'testable': False,
|
||||
'trackable': False,
|
||||
'virtual': False,
|
||||
}
|
||||
|
||||
self.post(url, data, expected_code=201)
|
||||
|
||||
# Attempting to create the exact same part again must be rejected cleanly
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertIn('non_field_errors', response.data)
|
||||
|
||||
def test_notes_on_create(self):
|
||||
"""Test that notes can be set when creating a Part."""
|
||||
list_url = reverse('api-part-list')
|
||||
|
||||
@@ -219,6 +219,40 @@ class PartTest(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
part_2.validate_unique()
|
||||
|
||||
def test_duplicate_no_ipn_revision(self):
|
||||
"""Test that we cannot create a duplicate Part when IPN and revision are both blank strings.
|
||||
|
||||
Regression test: the underlying 'unique_part' database constraint treats
|
||||
blank IPN/revision values (empty strings, as sent by the API when these
|
||||
fields are omitted) as equal, so validate_unique() must catch this case
|
||||
too, rather than raising an unhandled IntegrityError. Note that this is
|
||||
distinct from *unset* (None/NULL) values, which the database constraint
|
||||
does *not* treat as colliding (see test_revisions).
|
||||
"""
|
||||
cat = PartCategory.objects.get(pk=1)
|
||||
|
||||
Part.objects.create(
|
||||
category=cat,
|
||||
name='dupe_test',
|
||||
description='description',
|
||||
IPN='',
|
||||
revision='',
|
||||
)
|
||||
|
||||
part = Part(
|
||||
category=cat,
|
||||
name='dupe_test',
|
||||
description='description',
|
||||
IPN='',
|
||||
revision='',
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
part.validate_unique()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
part.save()
|
||||
|
||||
def test_attributes(self):
|
||||
"""Test Part attributes."""
|
||||
self.assertEqual(self.r1.name, 'R_2K2_0805')
|
||||
|
||||
Reference in New Issue
Block a user