2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

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!
This commit is contained in:
Oliver 2022-02-25 15:40:49 +11:00
parent 50a45474da
commit 44008f33e2
5 changed files with 55 additions and 94 deletions

View File

@ -479,8 +479,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
outputs = self.get_build_outputs(complete=True) outputs = self.get_build_outputs(complete=True)
# TODO - Ordering?
return outputs return outputs
@property @property
@ -491,8 +489,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
outputs = self.get_build_outputs(complete=False) outputs = self.get_build_outputs(complete=False)
# TODO - Order by how "complete" they are?
return outputs return outputs
@property @property
@ -563,7 +559,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
if self.remaining > 0: if self.remaining > 0:
return False return False
if not self.areUntrackedPartsFullyAllocated(): if not self.are_untracked_parts_allocated():
return False return False
# No issues! # No issues!
@ -584,7 +580,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
self.save() self.save()
# Remove untracked allocated stock # Remove untracked allocated stock
self.subtractUntrackedStock(user) self.subtract_allocated_stock(user)
# Ensure that there are no longer any BuildItem objects # Ensure that there are no longer any BuildItem objects
# which point to thisFcan Build Order # which point to thisFcan Build Order
@ -768,7 +764,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
output.delete() output.delete()
@transaction.atomic @transaction.atomic
def subtractUntrackedStock(self, user): def subtract_allocated_stock(self, user):
""" """
Called when the Build is marked as "complete", Called when the Build is marked as "complete",
this function removes the allocated untracked items from stock. this function removes the allocated untracked items from stock.
@ -831,7 +827,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
self.save() 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. 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) output - The particular build output (StockItem)
""" """
# Extract the BOM line item from the database quantity = bom_item.quantity
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
if output: if output:
quantity *= output.quantity quantity *= output.quantity
else: else:
quantity *= self.quantity quantity *= self.quantity
return 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 <part> to <output> Return all BuildItem objects which allocate stock of <bom_item> to <output>
Note that the bom_item may allow variants, or direct substitutes,
making things difficult.
Args: Args:
part - The part object bom_item - The BomItem object
output - Build output (StockItem). 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( allocations = BuildItem.objects.filter(
build=self, build=self,
stock_item__part__pk__in=[p.pk for p in variants], bom_item=bom_item,
install_into=output, install_into=output,
) )
return allocations 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. 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( allocated = allocations.aggregate(
q=Coalesce( q=Coalesce(
@ -891,24 +882,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
return allocated['q'] 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. Return the total unallocated (remaining) quantity of a part against a particular output.
""" """
required = self.requiredQuantity(part, output) required = self.required_quantity(bom_item, output)
allocated = self.allocatedQuantity(part, output) allocated = self.allocated_quantity(bom_item, output)
return max(required - allocated, 0) 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. Returns True if the particular build output is fully allocated.
""" """
@ -919,53 +910,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
else: else:
bom_items = self.tracked_bom_items bom_items = self.tracked_bom_items
fully_allocated = True
for bom_item in bom_items: for bom_item in bom_items:
part = bom_item.sub_part
if not self.isPartFullyAllocated(part, output): if not self.is_bom_item_allocated(bom_item, output):
fully_allocated = False return False
if verbose:
print(f"Part {part} is not fully allocated for output {output}")
else:
break
# All parts must be fully allocated! # 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 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 Return a list of bom items which have *not* 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
""" """
unallocated = [] unallocated = []
@ -977,10 +939,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
bom_items = self.tracked_bom_items bom_items = self.tracked_bom_items
for bom_item in bom_items: for bom_item in bom_items:
part = bom_item.sub_part
if not self.isPartFullyAllocated(part, output): if not self.is_bom_item_allocated(bom_item, output):
unallocated.append(part) unallocated.append(bom_item)
return unallocated return unallocated

View File

@ -160,7 +160,7 @@ class BuildOutputSerializer(serializers.Serializer):
if to_complete: if to_complete:
# The build output must have all tracked parts allocated # 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")) raise ValidationError(_("This build output is not fully allocated"))
return output return output
@ -436,7 +436,7 @@ class BuildCompleteSerializer(serializers.Serializer):
build = self.context['build'] 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')) raise ValidationError(_('Required stock has not been fully allocated'))
return value return value

View File

@ -125,7 +125,7 @@ src="{% static 'img/blank_image.png' %}"
{% trans "Required build quantity has not yet been completed" %} {% trans "Required build quantity has not yet been completed" %}
</div> </div>
{% endif %} {% endif %}
{% if not build.areUntrackedPartsFullyAllocated %} {% if not build.are_untracked_parts_allocated %}
<div class='alert alert-block alert-warning'> <div class='alert alert-block alert-warning'>
{% trans "Stock has not been fully allocated to this Build Order" %} {% trans "Stock has not been fully allocated to this Build Order" %}
</div> </div>
@ -234,7 +234,7 @@ src="{% static 'img/blank_image.png' %}"
{% else %} {% else %}
completeBuildOrder({{ build.pk }}, { 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 %}, completed: {% if build.remaining == 0 %}true{% else %}false{% endif %},
}); });
{% endif %} {% endif %}

View File

@ -192,7 +192,7 @@
<div class='panel-content'> <div class='panel-content'>
{% if build.has_untracked_bom_items %} {% if build.has_untracked_bom_items %}
{% if build.active %} {% if build.active %}
{% if build.areUntrackedPartsFullyAllocated %} {% if build.are_untracked_parts_allocated %}
<div class='alert alert-block alert-success'> <div class='alert alert-block alert-success'>
{% trans "Untracked stock has been fully allocated for this Build Order" %} {% trans "Untracked stock has been fully allocated for this Build Order" %}
</div> </div>

View File

@ -147,15 +147,15 @@ class BuildTest(TestCase):
# None of the build outputs have been completed # None of the build outputs have been completed
for output in self.build.get_build_outputs().all(): 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.is_bom_item_allocated(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_2, self.output_2))
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 15) self.assertEqual(self.build.unallocated_quantity(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.unallocated_quantity(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.unallocated_quantity(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_2, self.output_2), 21)
self.assertFalse(self.build.is_complete) 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 # Partially allocate tracked stock against build output 2
self.allocate_stock( 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 # Partially allocate untracked stock against build
self.allocate_stock( 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) 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.assertEqual(len(unallocated), 1)
self.build.unallocateStock() self.build.unallocateStock()
unallocated = self.build.unallocatedParts(None) unallocated = self.build.unallocated_bom_items(None)
self.assertEqual(len(unallocated), 2) 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 # Now we "fully" allocate the untracked untracked items
self.allocate_stock( 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): def test_cancel(self):
""" """
@ -331,9 +331,9 @@ class BuildTest(TestCase):
} }
) )
self.assertTrue(self.build.isFullyAllocated(None, verbose=True)) self.assertTrue(self.build.is_fully_allocated(None, verbose=True))
self.assertTrue(self.build.isFullyAllocated(self.output_1)) self.assertTrue(self.build.is_fully_allocated(self.output_1))
self.assertTrue(self.build.isFullyAllocated(self.output_2)) self.assertTrue(self.build.is_fully_allocated(self.output_2))
self.build.complete_build_output(self.output_1, None) self.build.complete_build_output(self.output_1, None)