From 44008f33e247bb856923d68b2b3fc9e7ff8b7dd3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Feb 2022 15:40:49 +1100 Subject: [PATCH] Refactoring Build model functions - Determining if a build order is correctly allocated has become more complex - Complex BOM behaviours (e.g. variants, templates, and substitutes) have made it more difficult! - Recently, a reference to the defining BomItem object was added to the BuildItem model - Now, a simpler way is to check allocation against the parent BomItem - It is much better, but means that a lot of refactoring and testing will be required! --- InvenTree/build/models.py | 101 ++++++------------ InvenTree/build/serializers.py | 4 +- .../build/templates/build/build_base.html | 4 +- InvenTree/build/templates/build/detail.html | 2 +- InvenTree/build/test_build.py | 38 +++---- 5 files changed, 55 insertions(+), 94 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 74b75787e7..f37b55876c 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -479,8 +479,6 @@ class Build(MPTTModel, ReferenceIndexingMixin): outputs = self.get_build_outputs(complete=True) - # TODO - Ordering? - return outputs @property @@ -491,8 +489,6 @@ class Build(MPTTModel, ReferenceIndexingMixin): outputs = self.get_build_outputs(complete=False) - # TODO - Order by how "complete" they are? - return outputs @property @@ -563,7 +559,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): if self.remaining > 0: return False - if not self.areUntrackedPartsFullyAllocated(): + if not self.are_untracked_parts_allocated(): return False # No issues! @@ -584,7 +580,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): self.save() # Remove untracked allocated stock - self.subtractUntrackedStock(user) + self.subtract_allocated_stock(user) # Ensure that there are no longer any BuildItem objects # which point to thisFcan Build Order @@ -768,7 +764,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): output.delete() @transaction.atomic - def subtractUntrackedStock(self, user): + def subtract_allocated_stock(self, user): """ Called when the Build is marked as "complete", this function removes the allocated untracked items from stock. @@ -831,7 +827,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): self.save() - def requiredQuantity(self, part, output): + def required_quantity(self, bom_item, output=None): """ Get the quantity of a part required to complete the particular build output. @@ -840,46 +836,41 @@ class Build(MPTTModel, ReferenceIndexingMixin): output - The particular build output (StockItem) """ - # Extract the BOM line item from the database - try: - bom_item = PartModels.BomItem.objects.get(part=self.part.pk, sub_part=part.pk) - quantity = bom_item.quantity - except (PartModels.BomItem.DoesNotExist): - quantity = 0 + quantity = bom_item.quantity if output: - quantity *= output.quantity + quantity *= output.quantity else: quantity *= self.quantity return quantity - def allocatedItems(self, part, output): + def allocated_bom_items(self, bom_item, output=None): """ - Return all BuildItem objects which allocate stock of to + Return all BuildItem objects which allocate stock of to + + Note that the bom_item may allow variants, or direct substitutes, + making things difficult. Args: - part - The part object + bom_item - The BomItem object output - Build output (StockItem). """ - # Remember, if 'variant' stock is allowed to be allocated, it becomes more complicated! - variants = part.get_descendants(include_self=True) - allocations = BuildItem.objects.filter( build=self, - stock_item__part__pk__in=[p.pk for p in variants], + bom_item=bom_item, install_into=output, ) return allocations - def allocatedQuantity(self, part, output): + def allocated_quantity(self, bom_item, output=None): """ Return the total quantity of given part allocated to a given build output. """ - allocations = self.allocatedItems(part, output) + allocations = self.allocated_bom_items(bom_item, output) allocated = allocations.aggregate( q=Coalesce( @@ -891,24 +882,24 @@ class Build(MPTTModel, ReferenceIndexingMixin): return allocated['q'] - def unallocatedQuantity(self, part, output): + def unallocated_quantity(self, bom_item, output=None): """ Return the total unallocated (remaining) quantity of a part against a particular output. """ - required = self.requiredQuantity(part, output) - allocated = self.allocatedQuantity(part, output) + required = self.required_quantity(bom_item, output) + allocated = self.allocated_quantity(bom_item, output) return max(required - allocated, 0) - def isPartFullyAllocated(self, part, output): + def is_bom_item_allocated(self, bom_item, output=None): """ - Returns True if the part has been fully allocated to the particular build output + Test if the supplied BomItem has been fully allocated! """ - return self.unallocatedQuantity(part, output) == 0 + return self.unallocated_quantity(bom_item, output) == 0 - def isFullyAllocated(self, output, verbose=False): + def is_fully_allocated(self, output, verbose=False): """ Returns True if the particular build output is fully allocated. """ @@ -919,53 +910,24 @@ class Build(MPTTModel, ReferenceIndexingMixin): 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): - fully_allocated = False - - if verbose: - print(f"Part {part} is not fully allocated for output {output}") - else: - break + if not self.is_bom_item_allocated(bom_item, output): + return False # All parts must be fully allocated! - return fully_allocated + return True - def areUntrackedPartsFullyAllocated(self): + def are_untracked_parts_allocated(self): """ Returns True if the un-tracked parts are fully allocated for this BuildOrder """ - return self.isFullyAllocated(None) + return self.is_fully_allocated(None) - def allocatedParts(self, output): + def unallocated_bom_items(self, output): """ - Return a list of parts which have been fully allocated against a particular output - """ - - allocated = [] - - # If output is not specified, we are talking about "untracked" items - if output is None: - bom_items = self.untracked_bom_items - else: - bom_items = self.tracked_bom_items - - for bom_item in bom_items: - part = bom_item.sub_part - - if self.isPartFullyAllocated(part, output): - allocated.append(part) - - return allocated - - def unallocatedParts(self, output): - """ - Return a list of parts which have *not* been fully allocated against a particular output + Return a list of bom items which have *not* been fully allocated against a particular output """ unallocated = [] @@ -977,10 +939,9 @@ class Build(MPTTModel, ReferenceIndexingMixin): bom_items = self.tracked_bom_items for bom_item in bom_items: - part = bom_item.sub_part - if not self.isPartFullyAllocated(part, output): - unallocated.append(part) + if not self.is_bom_item_allocated(bom_item, output): + unallocated.append(bom_item) return unallocated diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index e708bf0b3b..0c243a8e70 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -160,7 +160,7 @@ class BuildOutputSerializer(serializers.Serializer): if to_complete: # The build output must have all tracked parts allocated - if not build.isFullyAllocated(output): + if not build.is_fully_allocated(output): raise ValidationError(_("This build output is not fully allocated")) return output @@ -436,7 +436,7 @@ class BuildCompleteSerializer(serializers.Serializer): build = self.context['build'] - if not build.areUntrackedPartsFullyAllocated() and not value: + if not build.are_untracked_parts_allocated() and not value: raise ValidationError(_('Required stock has not been fully allocated')) return value diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 7340f1486d..cd7126a801 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -125,7 +125,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Required build quantity has not yet been completed" %} {% endif %} - {% if not build.areUntrackedPartsFullyAllocated %} + {% if not build.are_untracked_parts_allocated %}
{% trans "Stock has not been fully allocated to this Build Order" %}
@@ -234,7 +234,7 @@ src="{% static 'img/blank_image.png' %}" {% else %} completeBuildOrder({{ build.pk }}, { - allocated: {% if build.areUntrackedPartsFullyAllocated %}true{% else %}false{% endif %}, + allocated: {% if build.are_untracked_parts_allocated %}true{% else %}false{% endif %}, completed: {% if build.remaining == 0 %}true{% else %}false{% endif %}, }); {% endif %} diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 28760c5316..f85ec9afa6 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -192,7 +192,7 @@
{% if build.has_untracked_bom_items %} {% if build.active %} - {% if build.areUntrackedPartsFullyAllocated %} + {% if build.are_untracked_parts_allocated %}
{% trans "Untracked stock has been fully allocated for this Build Order" %}
diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 1a1f0b115e..e8578f9fbf 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -147,15 +147,15 @@ class BuildTest(TestCase): # None of the build outputs have been completed for output in self.build.get_build_outputs().all(): - self.assertFalse(self.build.isFullyAllocated(output)) + self.assertFalse(self.build.is_fully_allocated(output)) - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1)) - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2)) + self.assertFalse(self.build.is_bom_item_allocated(self.sub_part_1, self.output_1)) + self.assertFalse(self.build.is_bom_item_allocated(self.sub_part_2, self.output_2)) - self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 15) - self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 35) - self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 9) - self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 21) + self.assertEqual(self.build.unallocated_quantity(self.sub_part_1, self.output_1), 15) + self.assertEqual(self.build.unallocated_quantity(self.sub_part_1, self.output_2), 35) + self.assertEqual(self.build.unallocated_quantity(self.sub_part_2, self.output_1), 9) + self.assertEqual(self.build.unallocated_quantity(self.sub_part_2, self.output_2), 21) self.assertFalse(self.build.is_complete) @@ -226,7 +226,7 @@ class BuildTest(TestCase): } ) - self.assertTrue(self.build.isFullyAllocated(self.output_1)) + self.assertTrue(self.build.is_fully_allocated(self.output_1)) # Partially allocate tracked stock against build output 2 self.allocate_stock( @@ -236,7 +236,7 @@ class BuildTest(TestCase): } ) - self.assertFalse(self.build.isFullyAllocated(self.output_2)) + self.assertFalse(self.build.is_fully_allocated(self.output_2)) # Partially allocate untracked stock against build self.allocate_stock( @@ -247,9 +247,9 @@ class BuildTest(TestCase): } ) - self.assertFalse(self.build.isFullyAllocated(None, verbose=True)) + self.assertFalse(self.build.is_fully_allocated(None, verbose=True)) - unallocated = self.build.unallocatedParts(None) + unallocated = self.build.unallocated_bom_items(None) self.assertEqual(len(unallocated), 2) @@ -260,19 +260,19 @@ class BuildTest(TestCase): } ) - self.assertFalse(self.build.isFullyAllocated(None, verbose=True)) + self.assertFalse(self.build.is_fully_allocated(None, verbose=True)) - unallocated = self.build.unallocatedParts(None) + unallocated = self.build.unallocated_bom_items(None) self.assertEqual(len(unallocated), 1) self.build.unallocateStock() - unallocated = self.build.unallocatedParts(None) + unallocated = self.build.unallocated_bom_items(None) self.assertEqual(len(unallocated), 2) - self.assertFalse(self.build.areUntrackedPartsFullyAllocated()) + self.assertFalse(self.build.are_untracked_parts_allocated()) # Now we "fully" allocate the untracked untracked items self.allocate_stock( @@ -283,7 +283,7 @@ class BuildTest(TestCase): } ) - self.assertTrue(self.build.areUntrackedPartsFullyAllocated()) + self.assertTrue(self.build.are_untracked_parts_allocated()) def test_cancel(self): """ @@ -331,9 +331,9 @@ class BuildTest(TestCase): } ) - self.assertTrue(self.build.isFullyAllocated(None, verbose=True)) - self.assertTrue(self.build.isFullyAllocated(self.output_1)) - self.assertTrue(self.build.isFullyAllocated(self.output_2)) + self.assertTrue(self.build.is_fully_allocated(None, verbose=True)) + self.assertTrue(self.build.is_fully_allocated(self.output_1)) + self.assertTrue(self.build.is_fully_allocated(self.output_2)) self.build.complete_build_output(self.output_1, None)