diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 836536476b..d68064bed4 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -916,7 +916,7 @@ class Build(MPTTModel): return self.unallocatedQuantity(part, output) == 0 - def isFullyAllocated(self, output): + def isFullyAllocated(self, output, verbose=False): """ Returns True if the particular build output is fully allocated. """ @@ -927,14 +927,21 @@ class Build(MPTTModel): else: bom_items = self.tracked_bom_items + fully_allocated = True + for bom_item in bom_items: part = bom_item.sub_part if not self.isPartFullyAllocated(part, output): - return False + fully_allocated = False + + if verbose: + print(f"Part {part} is not fully allocated for output {output}") + else: + break # All parts must be fully allocated! - return True + return fully_allocated def areUntrackedPartsFullyAllocated(self): """ diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 1b7886c018..e336634625 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -19,6 +19,18 @@ class BuildTest(TestCase): def setUp(self): """ Initialize data to use for these tests. + + The base Part 'assembly' has a BOM consisting of three parts: + + - 5 x sub_part_1 + - 3 x sub_part_2 + - 2 x sub_part_3 (trackable) + + We will build 10x 'assembly' parts, in two build outputs: + + - 3 x output_1 + - 7 x output_2 + """ # Create a base "Part" @@ -41,17 +53,31 @@ class BuildTest(TestCase): component=True ) + self.sub_part_3 = Part.objects.create( + name="Widget C", + description="A widget", + component=True, + trackable=True + ) + # Create BOM item links for the parts BomItem.objects.create( part=self.assembly, sub_part=self.sub_part_1, - quantity=10 + quantity=5 ) BomItem.objects.create( part=self.assembly, sub_part=self.sub_part_2, - quantity=25 + quantity=3 + ) + + # sub_part_3 is trackable! + BomItem.objects.create( + part=self.assembly, + sub_part=self.sub_part_3, + quantity=2 ) # Create a "Build" object to make 10x objects @@ -64,14 +90,14 @@ class BuildTest(TestCase): # Create some build output (StockItem) objects self.output_1 = StockItem.objects.create( part=self.assembly, - quantity=5, + quantity=3, is_building=True, build=self.build ) self.output_2 = StockItem.objects.create( part=self.assembly, - quantity=5, + quantity=7, is_building=True, build=self.build, ) @@ -82,10 +108,12 @@ class BuildTest(TestCase): self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000) + self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000) + def test_init(self): # Perform some basic tests before we start the ball rolling - self.assertEqual(StockItem.objects.count(), 5) + self.assertEqual(StockItem.objects.count(), 6) # Build is PENDING self.assertEqual(self.build.status, status.BuildStatus.PENDING) @@ -144,84 +172,113 @@ class BuildTest(TestCase): quantity=99 ) - def allocate_stock(self, q11, q12, q21, output): - # Assign stock to this build + def allocate_stock(self, output, allocations): + """ + Allocate stock to this build, against a particular output - if q11 > 0: + Args: + output - StockItem object (or None) + allocations - Map of {StockItem: quantity} + """ + + for item, quantity in allocations.items(): BuildItem.objects.create( build=self.build, - stock_item=self.stock_1_1, - quantity=q11, + stock_item=item, + quantity=quantity, install_into=output ) - if q12 > 0: - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_1_2, - quantity=q12, - install_into=output - ) - - if q21 > 0: - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_2_1, - quantity=q21, - install_into=output, - ) - - # Attempt to create another identical BuildItem - b = BuildItem( - build=self.build, - stock_item=self.stock_2_1, - quantity=q21 - ) - - with self.assertRaises(ValidationError): - b.clean() - def test_partial_allocation(self): """ - Partially allocate against output 1 + Test partial allocation of stock """ - self.allocate_stock(50, 50, 200, self.output_1) + # Fully allocate tracked stock against build output 1 + self.allocate_stock( + self.output_1, + { + self.stock_3_1: 6, + } + ) self.assertTrue(self.build.isFullyAllocated(self.output_1)) + + # Partially allocate tracked stock against build output 2 + self.allocate_stock( + self.output_2, + { + self.stock_3_1: 1, + } + ) + self.assertFalse(self.build.isFullyAllocated(self.output_2)) - self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1)) - self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1)) - - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_2)) - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2)) - # Check that the part has been allocated - self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 100) + # Partially allocate untracked stock against build + self.allocate_stock( + None, + { + self.stock_1_1: 1, + self.stock_2_1: 1 + } + ) - self.build.unallocateStock(output=self.output_1) - self.assertEqual(BuildItem.objects.count(), 0) + self.assertFalse(self.build.isFullyAllocated(None, verbose=True)) - # Check that the part has been unallocated - self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 0) + unallocated = self.build.unallocatedParts(None) + + self.assertEqual(len(unallocated), 2) + + self.allocate_stock( + None, + { + self.stock_1_2: 100, + } + ) + + self.assertFalse(self.build.isFullyAllocated(None, verbose=True)) + + unallocated = self.build.unallocatedParts(None) + + self.assertEqual(len(unallocated), 1) + + self.build.unallocateUntracked() + + unallocated = self.build.unallocatedParts(None) + + self.assertEqual(len(unallocated), 2) + + self.assertFalse(self.build.areUntrackedPartsFullyAllocated()) + + # Now we "fully" allocate the untracked untracked items + self.allocate_stock( + None, + { + self.stock_1_1: 50, + self.stock_2_1: 50, + } + ) + + self.assertTrue(self.build.areUntrackedPartsFullyAllocated()) def test_auto_allocate(self): """ - Test auto-allocation functionality against the build outputs + Test auto-allocation functionality against the build outputs. + + Note: auto-allocations only work for un-tracked stock! """ - allocations = self.build.getAutoAllocations(self.output_1) + allocations = self.build.getAutoAllocations() self.assertEqual(len(allocations), 1) - self.build.autoAllocate(self.output_1) + self.build.autoAllocate() self.assertEqual(BuildItem.objects.count(), 1) - # Check that one part has been fully allocated to the build output - self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1)) + # Check that one un-tracked part has been fully allocated to the build + self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, None)) - # But, the *other* build output has not been allocated against - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2)) + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, None)) def test_cancel(self): """