From 183e6c37b1629173b3402b57e0c8014c139d0845 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 14 Jul 2026 12:41:40 +1000 Subject: [PATCH] Fix for BuildItem with zero quantity (#12387) --- src/backend/InvenTree/build/models.py | 6 +++ src/backend/InvenTree/build/test_build.py | 52 +++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 4e7a3efe7b..73751c6dca 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -2047,6 +2047,12 @@ class BuildItem(InvenTree.models.InvenTreeMetadataModel): if quantity > item.quantity: quantity = item.quantity + if quantity <= 0: + # There is nothing to consume or install: + # simply remove this (empty) allocation + self.delete() + return + # Split the allocated stock if there are more available than allocated if item.quantity > quantity: item = item.splitStock(quantity, None, user, notes=notes) diff --git a/src/backend/InvenTree/build/test_build.py b/src/backend/InvenTree/build/test_build.py index 79813954c6..00e134340b 100644 --- a/src/backend/InvenTree/build/test_build.py +++ b/src/backend/InvenTree/build/test_build.py @@ -711,6 +711,58 @@ class BuildTest(BuildTestBase): self.line_1.refresh_from_db() self.assertEqual(self.line_1.consumed, 8) + def test_complete_zero_quantity_allocation(self): + """A zero-quantity allocation is removed cleanly on completion. + + Regression test: completing a BuildItem with quantity=0 (permitted by + the model validators) crashed with an AttributeError, blocking build + completion until the empty allocation was manually removed. + """ + self.build.issue_build() + + # An allocation with zero quantity, against an item with stock available + alloc = BuildItem.objects.create( + build_line=self.line_1, stock_item=self.stock_1_2, quantity=0 + ) + + n_items = StockItem.objects.count() + + alloc.complete_allocation(user=self.user) + + # The empty allocation is deleted, with no stock operations performed + self.assertFalse(BuildItem.objects.filter(pk=alloc.pk).exists()) + self.assertEqual(StockItem.objects.count(), n_items) + + self.stock_1_2.refresh_from_db() + self.assertEqual(self.stock_1_2.quantity, 100) + self.assertIsNone(self.stock_1_2.consumed_by) + + self.line_1.refresh_from_db() + self.assertEqual(self.line_1.consumed, 0) + + # An allocation whose stock item has been depleted elsewhere + # is also removed cleanly (allocated quantity clamps to zero) + depleted = StockItem.objects.create( + part=self.sub_part_1, quantity=5, delete_on_deplete=False + ) + alloc = BuildItem.objects.create( + build_line=self.line_1, stock_item=depleted, quantity=5 + ) + + depleted.take_stock(5, self.user) + depleted.refresh_from_db() + self.assertEqual(depleted.quantity, 0) + + alloc.complete_allocation(user=self.user) + + self.assertFalse(BuildItem.objects.filter(pk=alloc.pk).exists()) + + depleted.refresh_from_db() + self.assertIsNone(depleted.consumed_by) + + self.line_1.refresh_from_db() + self.assertEqual(self.line_1.consumed, 0) + def test_complete_with_required_tests(self): """Test the prevention completion when a required test is missing feature.""" # with required tests incompleted the save should fail