diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index f83d836c6a..99d8dc6500 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -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 = [ diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index c2e772042d..d213431039 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -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): """ diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html index 241471e304..1455cc3525 100644 --- a/InvenTree/build/templates/build/auto_allocate.html +++ b/InvenTree/build/templates/build/auto_allocate.html @@ -7,8 +7,14 @@
{% trans "Automatically Allocate Stock" %}
-{% trans "Stock Items are selected for automatic allocation if there is only a single stock item available." %}
-{% trans "The following stock items will be allocated to the build:" %}
+{% trans "Where the following conditions are met, stock will be automatically allocated to this build" %}:
+
+{% trans "For each part in the BOM, the following tests are performed" %}:
+
{% if allocations %} diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 660b17ddc5..46bdc0ceaa 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -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 diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index de87b1c5e7..da07bda446 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -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: diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index ee7e579cce..598c818113 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -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; }