mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16: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:
parent
50a45474da
commit
44008f33e2
@ -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,12 +836,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
output - The particular build output (StockItem)
|
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
|
quantity = bom_item.quantity
|
||||||
except (PartModels.BomItem.DoesNotExist):
|
|
||||||
quantity = 0
|
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
quantity *= output.quantity
|
quantity *= output.quantity
|
||||||
@ -854,32 +845,32 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user