2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-03 13:58:47 +00:00

BOM tree fix (#5870)

* Extend protection against recurisve BOMs

- Prevent part variants from being use BOMs for other variants of the same part

* Add unit tests for new BOM validation checks

* Cleanup  urls.md

* Update unit tests

* Unit test update

* <ore unit test fixes
This commit is contained in:
Oliver 2023-11-07 09:48:04 +11:00 committed by GitHub
parent c34e14baec
commit 28399bed25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 3 deletions

View File

@ -483,14 +483,18 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
This will fail if: This will fail if:
a) The parent part is the same as this one a) The parent part is the same as this one
b) The parent part is used in the BOM for *this* part b) The parent part exists in the same variant tree as this one
c) The parent part is used in the BOM for any child parts under this one c) The parent part is used in the BOM for *this* part
d) The parent part is used in the BOM for any child parts under this one
""" """
result = True result = True
try: try:
if self.pk == parent.pk: if self.pk == parent.pk:
raise ValidationError({'sub_part': _(f"Part '{self}' is used in BOM for '{parent}' (recursive)")}) raise ValidationError({'sub_part': _(f"Part '{self}' cannot be used in BOM for '{parent}' (recursive)")})
if self.tree_id == parent.tree_id:
raise ValidationError({'sub_part': _(f"Part '{self}' cannot be used in BOM for '{parent}' (recursive)")})
bom_items = self.get_bom_items() bom_items = self.get_bom_items()

View File

@ -2196,6 +2196,13 @@ class BomItemTest(InvenTreeAPITestCase):
'part.delete', 'part.delete',
] ]
def setUp(self):
"""Set up the test case"""
super().setUp()
# Rebuild part tree so BOM items validate correctly
Part.objects.rebuild()
def test_bom_list(self): def test_bom_list(self):
"""Tests for the BomItem list endpoint.""" """Tests for the BomItem list endpoint."""
# How many BOM items currently exist in the database? # How many BOM items currently exist in the database?
@ -2357,6 +2364,7 @@ class BomItemTest(InvenTreeAPITestCase):
def test_get_bom_detail(self): def test_get_bom_detail(self):
"""Get the detail view for a single BomItem object.""" """Get the detail view for a single BomItem object."""
url = reverse('api-bom-item-detail', kwargs={'pk': 3}) url = reverse('api-bom-item-detail', kwargs={'pk': 3})
response = self.get(url, expected_code=200) response = self.get(url, expected_code=200)
@ -3032,6 +3040,11 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
'part_category.change', 'part_category.change',
] ]
def setUp(self):
"""Setup unit tets"""
super().setUp()
Part.objects.rebuild()
def metatester(self, apikey, model): def metatester(self, apikey, model):
"""Generic tester""" """Generic tester"""
modeldata = model.objects.first() modeldata = model.objects.first()

View File

@ -4,6 +4,7 @@ import csv
from django.urls import reverse from django.urls import reverse
import part.models
from InvenTree.unit_test import InvenTreeTestCase from InvenTree.unit_test import InvenTreeTestCase
@ -23,6 +24,8 @@ class BomExportTest(InvenTreeTestCase):
"""Perform test setup functions""" """Perform test setup functions"""
super().setUp() super().setUp()
part.models.Part.objects.rebuild()
self.url = reverse('api-bom-download', kwargs={'pk': 100}) self.url = reverse('api-bom-download', kwargs={'pk': 100})
def test_bom_template(self): def test_bom_template(self):

View File

@ -22,6 +22,8 @@ class BomUploadTest(InvenTreeAPITestCase):
"""Create BOM data as part of setup routine""" """Create BOM data as part of setup routine"""
super().setUpTestData() super().setUpTestData()
Part.objects.rebuild()
cls.part = Part.objects.create( cls.part = Part.objects.create(
name='Assembly', name='Assembly',
description='An assembled part', description='An assembled part',

View File

@ -28,6 +28,10 @@ class BomItemTest(TestCase):
def setUp(self): def setUp(self):
"""Create initial data""" """Create initial data"""
super().setUp()
Part.objects.rebuild()
self.bob = Part.objects.get(id=100) self.bob = Part.objects.get(id=100)
self.orphan = Part.objects.get(name='Orphan') self.orphan = Part.objects.get(name='Orphan')
self.r1 = Part.objects.get(name='R_2K2_0805') self.r1 = Part.objects.get(name='R_2K2_0805')
@ -261,3 +265,37 @@ class BomItemTest(TestCase):
p.set_metadata(k, k) p.set_metadata(k, k)
self.assertEqual(len(p.metadata.keys()), 4) self.assertEqual(len(p.metadata.keys()), 4)
def test_invalid_bom(self):
"""Test that ValidationError is correctly raised for an invalid BOM item"""
# First test: A BOM item which points to itself
with self.assertRaises(django_exceptions.ValidationError):
BomItem.objects.create(
part=self.bob,
sub_part=self.bob,
quantity=1
)
# Second test: A recursive BOM
part_a = Part.objects.create(name='Part A', description="A part which is called A", assembly=True, is_template=True, component=True)
part_b = Part.objects.create(name='Part B', description="A part which is called B", assembly=True, component=True)
part_c = Part.objects.create(name='Part C', description="A part which is called C", assembly=True, component=True)
BomItem.objects.create(part=part_a, sub_part=part_b, quantity=10)
BomItem.objects.create(part=part_b, sub_part=part_c, quantity=10)
with self.assertRaises(django_exceptions.ValidationError):
BomItem.objects.create(part=part_c, sub_part=part_a, quantity=10)
with self.assertRaises(django_exceptions.ValidationError):
BomItem.objects.create(part=part_c, sub_part=part_b, quantity=10)
# Third test: A recursive BOM with a variant part
part_v = Part.objects.create(name='Part V', description='A part which is called V', variant_of=part_a, assembly=True, component=True)
with self.assertRaises(django_exceptions.ValidationError):
BomItem.objects.create(part=part_a, sub_part=part_v, quantity=10)
with self.assertRaises(django_exceptions.ValidationError):
BomItem.objects.create(part=part_v, sub_part=part_a, quantity=10)