From 9e470d4064084b96730a4006321203f635be508a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 18 Apr 2021 21:41:34 +1000 Subject: [PATCH] Add separate section for "untracked" part allocation --- InvenTree/build/api.py | 8 +- InvenTree/build/forms.py | 6 - InvenTree/build/models.py | 72 ++++++++---- InvenTree/build/templates/build/allocate.html | 38 +++++++ InvenTree/build/templates/build/navbar.html | 2 +- InvenTree/build/views.py | 62 +++++------ InvenTree/templates/js/build.js | 104 ++++++++++++------ 7 files changed, 191 insertions(+), 101 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index e6331f2b6a..10cc7e2024 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -11,7 +11,7 @@ from rest_framework import generics from django.conf.urls import url, include -from InvenTree.helpers import str2bool +from InvenTree.helpers import str2bool, isNull from InvenTree.status_codes import BuildStatus from .models import Build, BuildItem @@ -194,7 +194,11 @@ class BuildItemList(generics.ListCreateAPIView): output = params.get('output', None) if output: - queryset = queryset.filter(install_into=output) + + if isNull(output): + queryset = queryset.filter(install_into=None) + else: + queryset = queryset.filter(install_into=output) return queryset diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 0726779b87..d4f5e23ce7 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -165,16 +165,10 @@ class AutoAllocateForm(HelperForm): confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation')) - # Keep track of which build output we are interested in - output = forms.ModelChoiceField( - queryset=StockItem.objects.all(), - ) - class Meta: model = Build fields = [ 'confirm', - 'output', ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 5a23752071..0fc92ce045 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -489,7 +489,7 @@ class Build(MPTTModel): self.status = BuildStatus.CANCELLED self.save() - def getAutoAllocations(self, output): + def getAutoAllocations(self): """ Return a list of StockItem objects which will be allocated using the 'AutoAllocate' function. @@ -521,15 +521,19 @@ class Build(MPTTModel): part = bom_item.sub_part + # If the part is "trackable" it cannot be auto-allocated + if part.trackable: + continue + # Skip any parts which are already fully allocated - if self.isPartFullyAllocated(part, output): + if self.isPartFullyAllocated(part, None): continue # How many parts are required to complete the output? - required = self.unallocatedQuantity(part, output) + required = self.unallocatedQuantity(part, None) # Grab a list of stock items which are available - stock_items = self.availableStockItems(part, output) + stock_items = self.availableStockItems(part, None) # Ensure that the available stock items are in the correct location if self.take_from is not None: @@ -544,7 +548,6 @@ class Build(MPTTModel): build_items = BuildItem.objects.filter( build=self, stock_item=stock_item, - install_into=output ) if len(build_items) > 0: @@ -567,24 +570,45 @@ class Build(MPTTModel): return allocations @transaction.atomic - def unallocateStock(self, output=None, part=None): + def unallocateOutput(self, output, part=None): """ - Deletes all stock allocations for this build. - - Args: - output: Specify which build output to delete allocations (optional) - + Unallocate all stock which are allocated against the provided "output" (StockItem) """ - allocations = BuildItem.objects.filter(build=self.pk) - - if output: - allocations = allocations.filter(install_into=output.pk) + allocations = BuildItem.objects.filter( + build=self, + install_into=output + ) if part: allocations = allocations.filter(stock_item__part=part) - # Remove all the allocations + allocations.delete() + + @transaction.atomic + def unallocateUntracked(self, part=None): + """ + Unallocate all "untracked" stock + """ + + allocations = BuildItem.objects.filter( + build=self, + install_into=None + ) + + if part: + allocations = allocations.filter(stock_item__part=part) + + allocations.delete() + + @transaction.atomic + def unallocateAll(self): + """ + Deletes all stock allocations for this build. + """ + + allocations = BuildItem.objects.filter(build=self) + allocations.delete() @transaction.atomic @@ -685,7 +709,7 @@ class Build(MPTTModel): output.delete() @transaction.atomic - def autoAllocate(self, output): + def autoAllocate(self): """ Run auto-allocation routine to allocate StockItems to this Build. @@ -702,7 +726,7 @@ class Build(MPTTModel): See: getAutoAllocations() """ - allocations = self.getAutoAllocations(output) + allocations = self.getAutoAllocations() for item in allocations: # Create a new allocation @@ -710,7 +734,7 @@ class Build(MPTTModel): build=self, stock_item=item['stock_item'], quantity=item['quantity'], - install_into=output, + install_into=None ) build_item.save() @@ -779,7 +803,7 @@ class Build(MPTTModel): if output: quantity *= output.quantity else: - quantity *= self.remaining + quantity *= self.quantity return quantity @@ -1020,10 +1044,12 @@ class BuildItem(models.Model): errors = {} - if not self.install_into: - raise ValidationError(_('Build item must specify a build output')) - try: + + # If the 'part' is trackable, then the 'install_into' field must be set! + if self.stock_item.part and self.stock_item.part.trackable and not self.install_into: + raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable')) + # Allocated part must be in the BOM for the master part if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False): errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)] diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 1b66f17d0d..fb4a8200e7 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -38,6 +38,41 @@
+ +
+ +
+
+
+
+
+
+ +
+ {% if build.incomplete_outputs %}
{% for item in build.incomplete_outputs %} @@ -66,6 +101,9 @@ part: {{ build.part.pk }}, }; + // Load allocation table for un-tracked parts + loadBuildOutputAllocationTable(buildInfo, null); + {% for item in build.incomplete_outputs %} // Get the build output as a javascript object inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, diff --git a/InvenTree/build/templates/build/navbar.html b/InvenTree/build/templates/build/navbar.html index 5e27010861..383f3c843f 100644 --- a/InvenTree/build/templates/build/navbar.html +++ b/InvenTree/build/templates/build/navbar.html @@ -27,7 +27,7 @@
  • - {% trans "In Progress" %} + {% trans "Allocate Stock" %}
  • {% endif %} diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 15ced77130..c467d374ba 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, extract_serial_numbers, normalize +from InvenTree.helpers import str2bool, extract_serial_numbers, normalize, isNull from InvenTree.status_codes import BuildStatus @@ -98,16 +98,6 @@ class BuildAutoAllocate(AjaxUpdateView): initials = super().get_initial() - # Pointing to a particular build output? - output = self.get_param('output') - - if output: - try: - output = StockItem.objects.get(pk=output) - initials['output'] = output - except (ValueError, StockItem.DoesNotExist): - pass - return initials def get_context_data(self, *args, **kwargs): @@ -121,16 +111,7 @@ class BuildAutoAllocate(AjaxUpdateView): form = self.get_form() - output_id = form['output'].value() - - try: - output = StockItem.objects.get(pk=output_id) - except (ValueError, StockItem.DoesNotExist): - output = None - - if output: - context['output'] = output - context['allocations'] = build.getAutoAllocations(output) + context['allocations'] = build.getAutoAllocations() context['build'] = build @@ -140,18 +121,11 @@ class BuildAutoAllocate(AjaxUpdateView): form = super().get_form() - if form['output'].value(): - # Hide the 'output' field - form.fields['output'].widget = HiddenInput() - return form def validate(self, build, form, **kwargs): - output = form.cleaned_data.get('output', None) - - if not output: - form.add_error(None, _('Build output must be specified')) + pass def save(self, build, form, **kwargs): """ @@ -159,9 +133,7 @@ class BuildAutoAllocate(AjaxUpdateView): perform auto-allocations """ - output = form.cleaned_data.get('output', None) - - build.autoAllocate(output) + build.autoAllocate() def get_data(self): return { @@ -365,10 +337,16 @@ class BuildUnallocate(AjaxUpdateView): output_id = request.POST.get('output_id', None) - try: - output = StockItem.objects.get(pk=output_id) - except (ValueError, StockItem.DoesNotExist): - output = None + if output_id: + + # If a "null" output is provided, we are trying to unallocate "untracked" stock + if isNull(output_id): + output = None + else: + try: + output = StockItem.objects.get(pk=output_id) + except (ValueError, StockItem.DoesNotExist): + output = None part_id = request.POST.get('part_id', None) @@ -383,9 +361,19 @@ class BuildUnallocate(AjaxUpdateView): form.add_error('confirm', _('Confirm unallocation of build stock')) form.add_error(None, _('Check the confirmation box')) else: - build.unallocateStock(output=output, part=part) + valid = True + # Unallocate the entire build + if not output_id: + build.unallocateAll() + # Unallocate a single output + elif output: + build.unallocateOutput(output, part=part) + # Unallocate "untracked" parts + else: + build.unallocateUntracked(part=part) + data = { 'form_valid': valid, } diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 539d1565aa..cdfd6756aa 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -37,7 +37,12 @@ function makeBuildOutputActionButtons(output, buildInfo) { */ var buildId = buildInfo.pk; - var outputId = output.pk; + + if (output) { + outputId = output.pk; + } else { + outputId = 'untracked'; + } var panel = `#allocation-panel-${outputId}`; @@ -50,35 +55,40 @@ function makeBuildOutputActionButtons(output, buildInfo) { var html = `
    `; - // Add a button to "auto allocate" against the build - html += makeIconButton( - 'fa-magic icon-blue', 'button-output-auto', outputId, - '{% trans "Auto-allocate stock items to this output" %}', - ); - - // Add a button to "complete" the particular build output - html += makeIconButton( - 'fa-check icon-green', 'button-output-complete', outputId, - '{% trans "Complete build output" %}', - { - //disabled: true - } - ); + // "Auto" allocation only works for untracked stock items + if (!output) { + html += makeIconButton( + 'fa-magic icon-blue', 'button-output-auto', outputId, + '{% trans "Auto-allocate stock items to this output" %}', + ); + } // Add a button to "cancel" the particular build output (unallocate) html += makeIconButton( 'fa-minus-circle icon-red', 'button-output-unallocate', outputId, '{% trans "Unallocate stock from build output" %}', - ); + ); - // Add a button to "delete" the particular build output - html += makeIconButton( - 'fa-trash-alt icon-red', 'button-output-delete', outputId, - '{% trans "Delete build output" %}', - ); - // Add a button to "destroy" the particular build output (mark as damaged, scrap) - // TODO + if (output) { + + // Add a button to "complete" the particular build output + html += makeIconButton( + 'fa-check icon-green', 'button-output-complete', outputId, + '{% trans "Complete build output" %}', + { + //disabled: true + } + ); + + // Add a button to "delete" the particular build output + html += makeIconButton( + 'fa-trash-alt icon-red', 'button-output-delete', outputId, + '{% trans "Delete build output" %}', + ); + + // TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap) + } html += '
    '; @@ -90,7 +100,6 @@ function makeBuildOutputActionButtons(output, buildInfo) { launchModalForm(`/build/${buildId}/auto-allocate/`, { data: { - output: outputId, }, success: reloadTable, } @@ -115,7 +124,7 @@ function makeBuildOutputActionButtons(output, buildInfo) { { success: reloadTable, data: { - output: outputId, + output: output ? outputId : 'null', } } ); @@ -152,13 +161,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var outputId = null; - outputId = output.pk; + if (output) { + outputId = output.pk; + } else { + outputId = 'untracked'; + } var table = options.table; if (options.table == null) { table = `#allocation-table-${outputId}`; } + + // If an "output" is specified, then only "trackable" parts are allocated + // Otherwise, only "untrackable" parts are allowed + var trackable = ! !output; function reloadTable() { // Reload the entire build allocation table @@ -168,7 +185,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { function requiredQuantity(row) { // Return the requied quantity for a given row - return row.quantity * output.quantity; + if (output) { + // "Tracked" parts are calculated against individual build outputs + return row.quantity * output.quantity; + } else { + // "Untracked" parts are specified against the build itself + return row.quantity * buildInfo.quantity; + } } function sumAllocations(row) { @@ -300,6 +323,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { queryParams: { part: partId, sub_part_detail: true, + sub_part_trackable: trackable, }, formatNoMatches: function() { return '{% trans "No BOM items found" %}'; @@ -310,11 +334,19 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output + var params = { + build: buildId, + } + + if (output) { + params.sub_part_trackable = true; + params.output = outputId; + } else { + params.sub_part_trackable = false; + } + inventreeGet('/api/build/item/', - { - build: buildId, - output: outputId, - }, + params, { success: function(data) { // Iterate through the returned data, and group by the part they point to @@ -355,8 +387,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Calculate the total allocated quantity var allocatedQuantity = sumAllocations(tableRow); + var requiredQuantity = 0; + + if (output) { + requiredQuantity = tableRow.quantity * output.quantity; + } else { + requiredQuantity = tableRow.quantity * buildInfo.quantity; + } + // Is this line item fully allocated? - if (allocatedQuantity >= (tableRow.quantity * output.quantity)) { + if (allocatedQuantity >= requiredQuantity) { allocatedLines += 1; }