diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 152df37b45..a9109c0174 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -2058,8 +2058,13 @@ class Part( 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). @@ -3923,9 +3928,12 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel): 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( @@ -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. """ return [ + 'part', 'part_id', + 'sub_part', 'sub_part_id', 'quantity', 'setup_quantity', diff --git a/src/backend/InvenTree/part/test_bom_item.py b/src/backend/InvenTree/part/test_bom_item.py index 7c3e40fb70..d2e8d91b91 100644 --- a/src/backend/InvenTree/part/test_bom_item.py +++ b/src/backend/InvenTree/part/test_bom_item.py @@ -390,3 +390,83 @@ class BomItemTest(TestCase): # 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)