mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Support structural part categories (#3931)
* Support structural part categories Structural categories cannot have parts assigned (only sub-categories) Fixed #3897 * Fixed unit test * Fix Oliver's review comments
This commit is contained in:
parent
1e1662ef0f
commit
73c1c50d01
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 81
|
||||
INVENTREE_API_VERSION = 82
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v82 -> 2022-11-16 : https://github.com/inventree/InvenTree/pull/3931
|
||||
- Add support for structural Part categories
|
||||
|
||||
v81 -> 2022-11-08 : https://github.com/inventree/InvenTree/pull/3710
|
||||
- Adds cached pricing information to Part API
|
||||
- Adds cached pricing information to BomItem API
|
||||
|
@ -702,7 +702,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
"""Perfofrm data validation for this item"""
|
||||
"""Perform data validation for this item"""
|
||||
super().validate(data)
|
||||
|
||||
build = self.context['build']
|
||||
|
@ -160,7 +160,8 @@ class CategoryList(APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
filterset_fields = [
|
||||
'name',
|
||||
'description'
|
||||
'description',
|
||||
'structural'
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
|
19
InvenTree/part/migrations/0090_auto_20221115_0816.py
Normal file
19
InvenTree/part/migrations/0090_auto_20221115_0816.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.16 on 2022-11-15 08:16
|
||||
|
||||
import common.settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0089_auto_20221112_0128'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='partcategory',
|
||||
name='structural',
|
||||
field=models.BooleanField(default=False, help_text="Parts may not be directly assigned to a structural category, but may be assigned to it's child categories.", verbose_name='Structural'),
|
||||
),
|
||||
]
|
@ -117,6 +117,14 @@ class PartCategory(MetadataMixin, InvenTreeTree):
|
||||
help_text=_('Default location for parts in this category')
|
||||
)
|
||||
|
||||
structural = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Structural'),
|
||||
help_text=_(
|
||||
'Parts may not be directly assigned to a structural category, '
|
||||
'but may be assigned to it\'s child categories.'),
|
||||
)
|
||||
|
||||
default_keywords = models.CharField(null=True, blank=True, max_length=250, verbose_name=_('Default keywords'), help_text=_('Default keywords for parts in this category'))
|
||||
|
||||
icon = models.CharField(
|
||||
@ -135,6 +143,17 @@ class PartCategory(MetadataMixin, InvenTreeTree):
|
||||
"""Return the web URL associated with the detail view for this PartCategory instance"""
|
||||
return reverse('category-detail', kwargs={'pk': self.id})
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean action for the PartCategory model:
|
||||
|
||||
- Ensure that the structural parameter cannot get set if products already assigned to the category
|
||||
"""
|
||||
if self.pk and self.structural and self.item_count > 0:
|
||||
raise ValidationError(
|
||||
_("You cannot make this part category structural because some parts "
|
||||
"are already assigned to it!"))
|
||||
super().clean()
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties"""
|
||||
verbose_name = _("Part Category")
|
||||
@ -424,6 +443,7 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
||||
If not, it is considered "orphaned" and will be deleted.
|
||||
"""
|
||||
# Get category templates settings
|
||||
|
||||
add_category_templates = kwargs.pop('add_category_templates', False)
|
||||
|
||||
if self.pk:
|
||||
@ -754,11 +774,17 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
||||
def clean(self):
|
||||
"""Perform cleaning operations for the Part model.
|
||||
|
||||
Update trackable status:
|
||||
- Check if the PartCategory is not structural
|
||||
|
||||
- Update trackable status:
|
||||
If this part is trackable, and it is used in the BOM
|
||||
for a parent part which is *not* trackable,
|
||||
then we will force the parent part to be trackable.
|
||||
"""
|
||||
if self.category is not None and self.category.structural:
|
||||
raise ValidationError(
|
||||
{'category': _("Parts cannot be assigned to structural part categories!")})
|
||||
|
||||
super().clean()
|
||||
|
||||
# Strip IPN field
|
||||
|
@ -75,6 +75,7 @@ class CategorySerializer(InvenTreeModelSerializer):
|
||||
'pathstring',
|
||||
'starred',
|
||||
'url',
|
||||
'structural',
|
||||
'icon',
|
||||
]
|
||||
|
||||
|
@ -4,6 +4,7 @@ from decimal import Decimal
|
||||
from enum import IntEnum
|
||||
from random import randint
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
import PIL
|
||||
@ -403,6 +404,59 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
child.refresh_from_db()
|
||||
self.assertEqual(child.parent, parent_category)
|
||||
|
||||
def test_structural(self):
|
||||
"""Test the effectiveness of structural categories
|
||||
|
||||
Make sure:
|
||||
- Parts cannot be created in structural categories
|
||||
- Parts cannot be assigned to structural categories
|
||||
"""
|
||||
|
||||
# Create our structural part category
|
||||
structural_category = PartCategory.objects.create(
|
||||
name='Structural category',
|
||||
description='This is the structural category',
|
||||
parent=None,
|
||||
structural=True
|
||||
)
|
||||
|
||||
part_count_before = Part.objects.count()
|
||||
|
||||
# Make sure that we get an error if we try to create part in the structural category
|
||||
with self.assertRaises(ValidationError):
|
||||
part = Part.objects.create(
|
||||
name="Part which shall not be created",
|
||||
description="-",
|
||||
category=structural_category
|
||||
)
|
||||
|
||||
# Ensure that the part really did not get created in the structural category
|
||||
self.assertEqual(part_count_before, Part.objects.count())
|
||||
|
||||
# Create a non structural category for test part category change
|
||||
non_structural_category = PartCategory.objects.create(
|
||||
name='Non-structural category',
|
||||
description='This is a non-structural category',
|
||||
parent=None,
|
||||
structural=False
|
||||
)
|
||||
|
||||
# Create the test part assigned to a non-structural category
|
||||
part = Part.objects.create(
|
||||
name="Part which category will be changed to structural",
|
||||
description="-",
|
||||
category=non_structural_category
|
||||
)
|
||||
|
||||
# Assign the test part to a structural category and make sure it gives an error
|
||||
part.category = structural_category
|
||||
with self.assertRaises(ValidationError):
|
||||
part.save()
|
||||
|
||||
# Ensure that the part did not get saved to the DB
|
||||
part.refresh_from_db()
|
||||
self.assertEqual(part.category.pk, non_structural_category.pk)
|
||||
|
||||
|
||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
"""Tests for the various OPTIONS endpoints in the /part/ API.
|
||||
|
@ -81,6 +81,9 @@ function partFields(options={}) {
|
||||
|
||||
return fields;
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
structural: false,
|
||||
}
|
||||
},
|
||||
name: {},
|
||||
@ -298,6 +301,7 @@ function categoryFields() {
|
||||
default_keywords: {
|
||||
icon: 'fa-key',
|
||||
},
|
||||
structural: {},
|
||||
icon: {
|
||||
help_text: `{% trans "Icon (optional) - Explore all available icons on" %} <a href="https://fontawesome.com/v5/search?s=solid" target="_blank" rel="noopener noreferrer">Font Awesome</a>.`,
|
||||
placeholder: 'fas fa-tag',
|
||||
|
Loading…
Reference in New Issue
Block a user