mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Fixes for "auto allocate" concept
This commit is contained in:
parent
551064b3a4
commit
a263d2fdcd
@ -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 = [
|
||||||
|
@ -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,
|
||||||
|
for the part to be auto-allocated:
|
||||||
|
|
||||||
- If there is a single StockItem, use that StockItem
|
- The sub_item in the BOM line must *not* be trackable
|
||||||
- Take as many parts as available (up to the quantity required for the BOM)
|
- There is only a single stock item available (which has not already been allocated to this build)
|
||||||
- If there are multiple StockItems available, ignore (leave up to the user)
|
- The stock item has an availability greater than zero
|
||||||
|
|
||||||
Args:
|
|
||||||
output: A stock item (build output) to auto-allocate against
|
|
||||||
|
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
@ -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 %}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -555,13 +555,24 @@ 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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user