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.'))
|
raise ValidationError(_('Duplicate part revision already exists.'))
|
||||||
|
|
||||||
# Ensure unique across (Name, revision, IPN) (as specified)
|
# Ensure unique across (Name, revision, IPN) (as specified)
|
||||||
if (self.revision or self.IPN) and (
|
# Note: the 'unique_part' database constraint only rejects a row when
|
||||||
Part.objects
|
# *all three* fields are non-null (Postgres treats NULL as distinct from
|
||||||
.exclude(pk=self.pk)
|
# NULL), so mirror that here rather than skipping whenever either field
|
||||||
.filter(name=self.name, revision=self.revision, IPN=self.IPN)
|
# is merely blank - an empty string ('') still collides with another
|
||||||
.exists()
|
# 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(
|
raise ValidationError(
|
||||||
_('Part with this Name, IPN and Revision already exists.')
|
_('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['active'])
|
||||||
self.assertFalse(response.data['purchaseable'])
|
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):
|
def test_notes_on_create(self):
|
||||||
"""Test that notes can be set when creating a Part."""
|
"""Test that notes can be set when creating a Part."""
|
||||||
list_url = reverse('api-part-list')
|
list_url = reverse('api-part-list')
|
||||||
|
|||||||
@@ -219,6 +219,40 @@ class PartTest(TestCase):
|
|||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
part_2.validate_unique()
|
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):
|
def test_attributes(self):
|
||||||
"""Test Part attributes."""
|
"""Test Part attributes."""
|
||||||
self.assertEqual(self.r1.name, 'R_2K2_0805')
|
self.assertEqual(self.r1.name, 'R_2K2_0805')
|
||||||
|
|||||||
Reference in New Issue
Block a user