mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-25 12:33: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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user