Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/backend/InvenTree/part/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2058,8 +2058,13 @@ def clear_bom(self):

Note: Does *NOT* delete inherited BOM items!
"""
import part.tasks as part_tasks

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):
"""Return a list of parts required to make this part (i.e. BOM items).

Expand Down Expand Up @@ -3923,9 +3928,12 @@ def save(self, *args, **kwargs):
assemblies = set()

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())

# Update the set of assemblies to include those which use this BomItem *after* we save
assemblies.update(self.get_assemblies())

for assembly in assemblies:
# Offload task to update the checksum for this assembly
InvenTree.tasks.offload_task(
Expand Down Expand Up @@ -4079,7 +4087,9 @@ def hash_fields(self) -> list[str]:
These fields are used to calculate the checksum hash of this BOM item.
"""
return [
'part',
'part_id',
'sub_part',
'sub_part_id',
'quantity',
'setup_quantity',
Expand Down
80 changes: 80 additions & 0 deletions src/backend/InvenTree/part/test_bom_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,83 @@ def test_locked_assembly(self):

# Delete the new BOM item
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)
Loading