2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 20:16:44 +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:
Miklós Márton 2022-11-17 22:10:06 +01:00 committed by GitHub
parent 1e1662ef0f
commit 73c1c50d01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 112 additions and 4 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version # 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 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 v81 -> 2022-11-08 : https://github.com/inventree/InvenTree/pull/3710
- Adds cached pricing information to Part API - Adds cached pricing information to Part API
- Adds cached pricing information to BomItem API - Adds cached pricing information to BomItem API

View File

@ -702,7 +702,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
] ]
def validate(self, data): def validate(self, data):
"""Perfofrm data validation for this item""" """Perform data validation for this item"""
super().validate(data) super().validate(data)
build = self.context['build'] build = self.context['build']

View File

@ -160,7 +160,8 @@ class CategoryList(APIDownloadMixin, ListCreateAPI):
filterset_fields = [ filterset_fields = [
'name', 'name',
'description' 'description',
'structural'
] ]
ordering_fields = [ ordering_fields = [

View 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'),
),
]

View File

@ -117,6 +117,14 @@ class PartCategory(MetadataMixin, InvenTreeTree):
help_text=_('Default location for parts in this category') 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')) 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( 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 the web URL associated with the detail view for this PartCategory instance"""
return reverse('category-detail', kwargs={'pk': self.id}) 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: class Meta:
"""Metaclass defines extra model properties""" """Metaclass defines extra model properties"""
verbose_name = _("Part Category") verbose_name = _("Part Category")
@ -424,6 +443,7 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
If not, it is considered "orphaned" and will be deleted. If not, it is considered "orphaned" and will be deleted.
""" """
# Get category templates settings # Get category templates settings
add_category_templates = kwargs.pop('add_category_templates', False) add_category_templates = kwargs.pop('add_category_templates', False)
if self.pk: if self.pk:
@ -754,11 +774,17 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
def clean(self): def clean(self):
"""Perform cleaning operations for the Part model. """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 If this part is trackable, and it is used in the BOM
for a parent part which is *not* trackable, for a parent part which is *not* trackable,
then we will force the parent part to be 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() super().clean()
# Strip IPN field # Strip IPN field

View File

@ -75,6 +75,7 @@ class CategorySerializer(InvenTreeModelSerializer):
'pathstring', 'pathstring',
'starred', 'starred',
'url', 'url',
'structural',
'icon', 'icon',
] ]

View File

@ -4,6 +4,7 @@ from decimal import Decimal
from enum import IntEnum from enum import IntEnum
from random import randint from random import randint
from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
import PIL import PIL
@ -403,6 +404,59 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
child.refresh_from_db() child.refresh_from_db()
self.assertEqual(child.parent, parent_category) 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): class PartOptionsAPITest(InvenTreeAPITestCase):
"""Tests for the various OPTIONS endpoints in the /part/ API. """Tests for the various OPTIONS endpoints in the /part/ API.

View File

@ -81,6 +81,9 @@ function partFields(options={}) {
return fields; return fields;
} }
},
filters: {
structural: false,
} }
}, },
name: {}, name: {},
@ -298,6 +301,7 @@ function categoryFields() {
default_keywords: { default_keywords: {
icon: 'fa-key', icon: 'fa-key',
}, },
structural: {},
icon: { 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>.`, 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', placeholder: 'fas fa-tag',