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')) 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: class Meta:
model = BuildItem model = BuildItem
fields = [ fields = [

View File

@ -183,6 +183,14 @@ class Build(MPTTModel):
blank=True, help_text=_('Extra build notes') 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 @property
def output_count(self): def output_count(self):
return self.build_outputs.count() return self.build_outputs.count()
@ -333,20 +341,25 @@ class Build(MPTTModel):
self.save() self.save()
def getAutoAllocations(self, output): 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. using the 'AutoAllocate' function.
For each item in the BOM for the attached Part: For each item in the BOM for the attached Part,
the following tests must *all* evaluate to True,
- If there is a single StockItem, use that StockItem for the part to be auto-allocated:
- 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
- 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: 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 = [] allocations = []
@ -354,24 +367,42 @@ class Build(MPTTModel):
""" """
Iterate through each item in the BOM 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? # Only look at the "untracked" BOM items
q_required = item.quantity * self.quantity # 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" # 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 # Filter by part reference
stock = stock.filter(part=item.sub_part) 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 # Ensure that the available stock items are in the correct location
if self.take_from is not None: if self.take_from is not None:
# Filter for stock that is located downstream of the designated location # Filter for stock that is located downstream of the designated location
stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()]) stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])
# Only one StockItem to choose from? Default to that one! # Only one StockItem to choose from? Default to that one!
if len(stock) == 1: if stock.count() == 1:
stock_item = stock[0] stock_item = stock[0]
# Check that we have not already allocated this stock-item against this build # Check that we have not already allocated this stock-item against this build
@ -567,7 +598,7 @@ class Build(MPTTModel):
if output: if output:
return q * output.quantity return q * output.quantity
else: else:
return q * self.quantity return q * self.remaining
def getAllocatedQuantity(self, part, output=None): def getAllocatedQuantity(self, part, output=None):
""" """

View File

@ -7,8 +7,14 @@
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>
<b>{% trans "Automatically Allocate Stock" %}</b><br> <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 "Where the following conditions are met, stock will be automatically allocated to this build" %}:<br>
{% trans "The following stock items will be allocated to the 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> </div>
{% if allocations %} {% 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 AjaxUpdateView, AjaxCreateView, AjaxDeleteView
from InvenTree.views import InvenTreeRoleMixin from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import str2bool, ExtractSerialNumbers from InvenTree.helpers import str2bool, ExtractSerialNumbers, normalize
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
@ -116,10 +116,12 @@ class BuildAutoAllocate(AjaxUpdateView):
context = {} context = {}
output = self.get_form()['output_id'].value()
try: try:
build = Build.objects.get(id=self.kwargs['pk']) build = Build.objects.get(id=self.kwargs['pk'])
context['build'] = build context['build'] = build
context['allocations'] = build.getAutoAllocations() context['allocations'] = build.getAutoAllocations(output)
except Build.DoesNotExist: except Build.DoesNotExist:
context['error'] = _('No matching build found') context['error'] = _('No matching build found')
@ -687,6 +689,26 @@ class BuildItemCreate(AjaxCreateView):
return ctx 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): def get_form(self):
""" Create Form for making / editing new Part object """ """ Create Form for making / editing new Part object """
@ -715,7 +737,7 @@ class BuildItemCreate(AjaxCreateView):
pass pass
# If the sub_part is supplied, limit to matching stock items # 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: if part_id:
try: try:
@ -781,7 +803,7 @@ class BuildItemCreate(AjaxCreateView):
if part_id: if part_id:
try: try:
part = Part.objects.get(pk=part_id) part = Part.objects.get(pk=part_id)
initials['part'] = part initials['part_id'] = part.pk
except Part.DoesNotExist: except Part.DoesNotExist:
pass pass

View File

@ -726,6 +726,15 @@ class StockItem(MPTTModel):
@property @property
def in_stock(self): 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 # Not 'in stock' if it has been installed inside another StockItem
if self.belongs_to is not None: if self.belongs_to is not None:

View File

@ -555,12 +555,23 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
qB *= buildInfo.quantity; 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; 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 progressA = parseFloat(aA) / qA;
var progressB = parseFloat(aB) / qB; 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; return (progressA < progressB) ? 1 : -1;
} }