2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-25 04:23:33 +00:00

[bug] BOM validation fixes (#11783)

* Ensure a new BOM item triggers check_bom_valid

* Ensure BOM validation is properly recomputed on a BOM item update

* Add unit tests

* Tweak unit tests

* Update 'clear_bom' method
This commit is contained in:
Oliver
2026-04-22 22:44:48 +10:00
committed by GitHub
parent d8cd1849ba
commit a94798daaa
2 changed files with 91 additions and 1 deletions
+11 -1
View File
@@ -2058,8 +2058,13 @@ class Part(
Note: Does *NOT* delete inherited BOM items! Note: Does *NOT* delete inherited BOM items!
""" """
import part.tasks as part_tasks
self.bom_items.all().delete() self.bom_items.all().delete()
# Offload task to re-validate the BOM for this assembly
InvenTree.tasks.offload_task(part_tasks.check_bom_valid, self.pk, group='part')
def getRequiredParts(self, recursive=False, parts=None): def getRequiredParts(self, recursive=False, parts=None):
"""Return a list of parts required to make this part (i.e. BOM items). """Return a list of parts required to make this part (i.e. BOM items).
@@ -3923,9 +3928,12 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
assemblies = set() assemblies = set()
if db_instance: if db_instance:
# Find all assemblies which use this BomItem *after* we save # Find all assemblies which use this BomItem *before* we save
assemblies.update(db_instance.get_assemblies()) assemblies.update(db_instance.get_assemblies())
# Update the set of assemblies to include those which use this BomItem *after* we save
assemblies.update(self.get_assemblies())
for assembly in assemblies: for assembly in assemblies:
# Offload task to update the checksum for this assembly # Offload task to update the checksum for this assembly
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
@@ -4079,7 +4087,9 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
These fields are used to calculate the checksum hash of this BOM item. These fields are used to calculate the checksum hash of this BOM item.
""" """
return [ return [
'part',
'part_id', 'part_id',
'sub_part',
'sub_part_id', 'sub_part_id',
'quantity', 'quantity',
'setup_quantity', 'setup_quantity',
@@ -390,3 +390,83 @@ class BomItemTest(TestCase):
# Delete the new BOM item # Delete the new BOM item
bom_item.delete() bom_item.delete()
def test_bom_validated(self):
"""Test for caching of 'bom_validated' property."""
from part.tasks import validate_bom
assembly = Part.objects.create(
name='Assembly1', description='An assembly part', assembly=True
)
assembly_2 = Part.objects.create(
name='Assembly2', description='An assembly part', assembly=True
)
def check(valid: bool = True):
"""Helper function to check the BOM for this assembly."""
nonlocal assembly
assembly.refresh_from_db()
self.assertEqual(assembly.bom_validated, valid)
def validate(valid: bool = True):
"""Helper function to validate the BOM for this assembly."""
nonlocal assembly
validate_bom(assembly.pk, valid)
check(valid)
check(valid=False)
validate()
sub_part_1 = Part.objects.create(
name='SubPart1', description='A sub-part', component=True
)
sub_part_2 = Part.objects.create(
name='SubPart2', description='A sub-part', component=True
)
# Still valid at this stage - we have not made any changes to the BOM
check(valid=True)
# Creating a *new* BOM item should invalidate the bom_validated cache
bom_item = BomItem.objects.create(
part=assembly, sub_part=sub_part_1, quantity=1
)
check(valid=False)
# Editing the BOM item should also invalidate the bom_validated cache
validate()
bom_item.quantity = 2
bom_item.save()
check(valid=False)
# Editing the BOM item without changing any relevant fields should not invalidate the bom_validated cache
validate()
bom_item.description = 'This is a description'
bom_item.save()
check(valid=True)
# Point the BOM item to a different component
validate()
bom_item.sub_part = sub_part_2
bom_item.save()
check(valid=False)
# Point the BOM to a different assembly
validate()
bom_item.part = assembly_2
bom_item.save()
check(valid=False)
# Check a partial restore - returning to previous state should re-validate
bom_item.part = assembly
bom_item.save()
check(valid=True)
# Now, delete the BomItem entirely
bom_item.delete()
check(valid=False)
self.assertIsNotNone(assembly.bom_checked_date)