mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 21:15:41 +00:00
Merge branch 'master' into variant-available
# Conflicts: # InvenTree/InvenTree/api_version.py
This commit is contained in:
@ -1175,6 +1175,18 @@ class PartList(generics.ListCreateAPIView):
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by 'variant_of'
|
||||
# Note that this is subtly different from 'ancestor' filter (above)
|
||||
variant_of = params.get('variant_of', None)
|
||||
|
||||
if variant_of is not None:
|
||||
try:
|
||||
template = Part.objects.get(pk=variant_of)
|
||||
variants = template.get_children()
|
||||
queryset = queryset.filter(pk__in=[v.pk for v in variants])
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter only parts which are in the "BOM" for a given part
|
||||
in_bom_for = params.get('in_bom_for', None)
|
||||
|
||||
@ -1339,10 +1351,6 @@ class PartList(generics.ListCreateAPIView):
|
||||
filters.OrderingFilter,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'variant_of',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
'name',
|
||||
'creation_date',
|
||||
|
@ -177,6 +177,7 @@
|
||||
fields:
|
||||
name: 'Green chair variant'
|
||||
variant_of: 10003
|
||||
is_template: true
|
||||
category: 7
|
||||
trackable: true
|
||||
tree_id: 1
|
||||
|
@ -777,7 +777,8 @@ class Part(MPTTModel):
|
||||
# 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 self.IPN is not None and not allow_duplicate_ipn:
|
||||
# Raise an error if an IPN is set, and it is a duplicate
|
||||
if self.IPN and not allow_duplicate_ipn:
|
||||
parts = Part.objects.filter(IPN__iexact=self.IPN)
|
||||
parts = parts.exclude(pk=self.pk)
|
||||
|
||||
@ -798,6 +799,10 @@ class Part(MPTTModel):
|
||||
|
||||
super().clean()
|
||||
|
||||
# Strip IPN field
|
||||
if type(self.IPN) is str:
|
||||
self.IPN = self.IPN.strip()
|
||||
|
||||
if self.trackable:
|
||||
for part in self.get_used_in().all():
|
||||
|
||||
|
@ -567,6 +567,97 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['name'], name)
|
||||
self.assertEqual(response.data['description'], description)
|
||||
|
||||
def test_template_filters(self):
|
||||
"""
|
||||
Unit tests for API filters related to template parts:
|
||||
|
||||
- variant_of : Return children of specified part
|
||||
- ancestor : Return descendants of specified part
|
||||
|
||||
Uses the 'chair template' part (pk=10000)
|
||||
"""
|
||||
|
||||
# Rebuild the MPTT structure before running these tests
|
||||
Part.objects.rebuild()
|
||||
|
||||
url = reverse('api-part-list')
|
||||
|
||||
response = self.get(
|
||||
url,
|
||||
{
|
||||
'variant_of': 10000,
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
# 3 direct children of template part
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
response = self.get(
|
||||
url,
|
||||
{
|
||||
'ancestor': 10000,
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
# 4 total descendants
|
||||
self.assertEqual(len(response.data), 4)
|
||||
|
||||
# Use the 'green chair' as our reference
|
||||
response = self.get(
|
||||
url,
|
||||
{
|
||||
'variant_of': 10003,
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
response = self.get(
|
||||
url,
|
||||
{
|
||||
'ancestor': 10003,
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
# Add some more variants
|
||||
|
||||
p = Part.objects.get(pk=10004)
|
||||
|
||||
for i in range(100):
|
||||
Part.objects.create(
|
||||
name=f'Chair variant {i}',
|
||||
description='A new chair variant',
|
||||
variant_of=p,
|
||||
)
|
||||
|
||||
# There should still be only one direct variant
|
||||
response = self.get(
|
||||
url,
|
||||
{
|
||||
'variant_of': 10003,
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
# However, now should be 101 descendants
|
||||
response = self.get(
|
||||
url,
|
||||
{
|
||||
'ancestor': 10003,
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 101)
|
||||
|
||||
|
||||
class PartDetailTests(InvenTreeAPITestCase):
|
||||
"""
|
||||
|
@ -349,6 +349,26 @@ class PartSettingsTest(TestCase):
|
||||
part = Part(name='Hello', description='A thing', IPN='IPN123', revision='C')
|
||||
part.full_clean()
|
||||
|
||||
# Any duplicate IPN should raise an error
|
||||
Part.objects.create(name='xyz', revision='1', description='A part', IPN='UNIQUE')
|
||||
|
||||
# Case insensitive, so variations on spelling should throw an error
|
||||
for ipn in ['UNiquE', 'uniQuE', 'unique']:
|
||||
with self.assertRaises(ValidationError):
|
||||
Part.objects.create(name='xyz', revision='2', description='A part', IPN=ipn)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
Part.objects.create(name='zyx', description='A part', IPN='UNIQUE')
|
||||
|
||||
# However, *blank* / empty IPN values should be allowed, even if duplicates are not
|
||||
# Note that leading / trailling whitespace characters are trimmed, too
|
||||
Part.objects.create(name='abc', revision='1', description='A part', IPN=None)
|
||||
Part.objects.create(name='abc', revision='2', description='A part', IPN='')
|
||||
Part.objects.create(name='abc', revision='3', description='A part', IPN=None)
|
||||
Part.objects.create(name='abc', revision='4', description='A part', IPN=' ')
|
||||
Part.objects.create(name='abc', revision='5', description='A part', IPN=' ')
|
||||
Part.objects.create(name='abc', revision='6', description='A part', IPN=' ')
|
||||
|
||||
|
||||
class PartSubscriptionTests(TestCase):
|
||||
|
||||
|
Reference in New Issue
Block a user