Fixes for "auto allocate" concept

This commit is contained in:
Oliver Walters 2020-10-29 00:49:01 +11:00
parent 551064b3a4
commit a263d2fdcd
6 changed files with 104 additions and 23 deletions

View File

@ -172,6 +172,8 @@ class EditBuildItemForm(HelperForm):
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, help_text=_('Select quantity of stock to allocate'))
part_id = forms.IntegerField(required=False, widget=forms.HiddenInput())
class Meta:
model = BuildItem
fields = [

View File

@ -183,6 +183,14 @@ class Build(MPTTModel):
blank=True, help_text=_('Extra build notes')
)
@property
def remaining(self):
"""
Return the number of outputs remaining to be completed.
"""
return max(0, self.quantity - self.completed)
@property
def output_count(self):
return self.build_outputs.count()
@ -333,20 +341,25 @@ class Build(MPTTModel):
self.save()
def getAutoAllocations(self, output):
""" Return a list of parts which will be allocated
"""
Return a list of StockItem objects which will be allocated
using the 'AutoAllocate' function.
For each item in the BOM for the attached Part:
- If there is a single StockItem, use that StockItem
- Take as many parts as available (up to the quantity required for the BOM)
- If there are multiple StockItems available, ignore (leave up to the user)
Args:
output: A stock item (build output) to auto-allocate against
For each item in the BOM for the attached Part,
the following tests must *all* evaluate to True,
for the part to be auto-allocated:
- The sub_item in the BOM line must *not* be trackable
- There is only a single stock item available (which has not already been allocated to this build)
- The stock item has an availability greater than zero
Returns:
A list object containing the StockItem objects to be allocated (and the quantities)
A list object containing the StockItem objects to be allocated (and the quantities).
Each item in the list is a dict as follows:
{
'stock_item': stock_item,
'quantity': stock_quantity,
}
"""
allocations = []
@ -354,24 +367,42 @@ class Build(MPTTModel):
"""
Iterate through each item in the BOM
"""
for item in self.part.bom_items.all().prefetch_related('sub_part'):
# How many parts required for this build?
q_required = item.quantity * self.quantity
# Only look at the "untracked" BOM items
# Tracked BOM items must be handled separately
untracked_bom_items = self.part.bom_items.filter(sub_part__trackable=False)
for item in untracked_bom_items.prefetch_related('sub_part'):
# How many parts are still required for this build?
#q_required = item.quantity * self.remaining
q_required = self.getUnallocatedQuantity(item.sub_part)
# Grab a list of StockItem objects which are "in stock"
stock = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER)
stock = StockModels.StockItem.objects.filter(
StockModels.StockItem.IN_STOCK_FILTER
)
# Filter by part reference
stock = stock.filter(part=item.sub_part)
# Exclude any stock items which have already been allocated to this build
allocated = BuildItem.objects.filter(
build=self,
stock_item__part=item.sub_part
)
stock = stock.exclude(
pk__in=[build_item.stock_item.pk for build_item in allocated]
)
# Ensure that the available stock items are in the correct location
if self.take_from is not None:
# Filter for stock that is located downstream of the designated location
stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])
# Only one StockItem to choose from? Default to that one!
if len(stock) == 1:
if stock.count() == 1:
stock_item = stock[0]
# Check that we have not already allocated this stock-item against this build
@ -567,7 +598,7 @@ class Build(MPTTModel):
if output:
return q * output.quantity
else:
return q * self.quantity
return q * self.remaining
def getAllocatedQuantity(self, part, output=None):
"""

View File

@ -7,8 +7,14 @@
<div class='alert alert-block alert-info'>
<b>{% trans "Automatically Allocate Stock" %}</b><br>
{% trans "Stock Items are selected for automatic allocation if there is only a single stock item available." %}<br>
{% trans "The following stock items will be allocated to the build:" %}<br>
{% trans "Where the following conditions are met, stock will be automatically allocated to this build" %}:<br>
<hr>
{% trans "For each part in the BOM, the following tests are performed" %}:<br>
<ul>
<li>{% trans "The part is not marked as trackable" %}</li>
<li>{% trans "Only single stock items exists" %}</li>
<li>{% trans "The stock item is not already allocated to this build" %}</li>
</ul>
</div>
{% if allocations %}

View File

@ -18,7 +18,7 @@ from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import str2bool, ExtractSerialNumbers
from InvenTree.helpers import str2bool, ExtractSerialNumbers, normalize
from InvenTree.status_codes import BuildStatus
@ -116,10 +116,12 @@ class BuildAutoAllocate(AjaxUpdateView):
context = {}
output = self.get_form()['output_id'].value()
try:
build = Build.objects.get(id=self.kwargs['pk'])
context['build'] = build
context['allocations'] = build.getAutoAllocations()
context['allocations'] = build.getAutoAllocations(output)
except Build.DoesNotExist:
context['error'] = _('No matching build found')
@ -687,6 +689,26 @@ class BuildItemCreate(AjaxCreateView):
return ctx
def validate(self, request, form, data):
"""
Extra validation steps as required
"""
stock_item = data.get('stock_item', None)
quantity = data.get('quantity', None)
if stock_item:
# Stock item must actually be in stock!
if not stock_item.in_stock:
form.add_error('stock_item', _('Item must be currently in stock'))
# Check that there are enough items available
if quantity is not None:
available = stock_item.unallocated_quantity()
if quantity > available:
form.add_error('stock_item', _('Stock item is over-allocated'))
form.add_error('quantity', _('Avaialabe') + ': ' + str(normalize(available)))
def get_form(self):
""" Create Form for making / editing new Part object """
@ -715,7 +737,7 @@ class BuildItemCreate(AjaxCreateView):
pass
# If the sub_part is supplied, limit to matching stock items
part_id = self.get_param('part')
part_id = form['part_id'].value()
if part_id:
try:
@ -781,7 +803,7 @@ class BuildItemCreate(AjaxCreateView):
if part_id:
try:
part = Part.objects.get(pk=part_id)
initials['part'] = part
initials['part_id'] = part.pk
except Part.DoesNotExist:
pass

View File

@ -726,6 +726,15 @@ class StockItem(MPTTModel):
@property
def in_stock(self):
"""
Returns True if this item is in stock
See also: IN_STOCK_FILTER
"""
# Quantity must be above zero (unless infinite)
if self.quantity <= 0 and not self.infinite:
return False
# Not 'in stock' if it has been installed inside another StockItem
if self.belongs_to is not None:

View File

@ -555,12 +555,23 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
qB *= buildInfo.quantity;
}
if (aA == 0 && aB == 0) {
// Handle the case where both numerators are zero
if ((aA == 0) && (aB == 0)) {
return (qA > qB) ? 1 : -1;
}
// Handle the case where either denominator is zero
if ((qA == 0) || (qB == 0)) {
return 1;
}
var progressA = parseFloat(aA) / qA;
var progressB = parseFloat(aB) / qB;
// Handle the case where both are at 100%
if (progressA == 1.0 && progressB == 1.0) {
return (qA < qB) ? 1 : -1;
}
return (progressA < progressB) ? 1 : -1;
}