diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index e39d0ba9cf..db4188021d 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -8,6 +8,8 @@ from __future__ import unicode_literals from django.utils.translation import ugettext as _ from InvenTree.forms import HelperForm +from InvenTree.fields import RoundingDecimalFormField + from django import forms from .models import Build, BuildItem from stock.models import StockLocation @@ -91,7 +93,7 @@ class CompleteBuildForm(HelperForm): class CancelBuildForm(HelperForm): """ Form for cancelling a build """ - confirm_cancel = forms.BooleanField(required=False, help_text='Confirm build cancellation') + confirm_cancel = forms.BooleanField(required=False, help_text=_('Confirm build cancellation')) class Meta: model = Build @@ -101,7 +103,11 @@ class CancelBuildForm(HelperForm): class EditBuildItemForm(HelperForm): - """ Form for adding a new BuildItem to a Build """ + """ + Form for creating (or editing) a BuildItem object. + """ + + quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, help_text=_('Select quantity of stock to allocate')) class Meta: model = BuildItem @@ -109,4 +115,5 @@ class EditBuildItemForm(HelperForm): 'build', 'stock_item', 'quantity', + 'install_into', ] diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index bf43d0ca85..71c627e0e8 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -20,14 +20,39 @@ InvenTree | Allocate Parts {% endif %} - -
+ + + + +
+ {% for item in build.incomplete_outputs %} + {% include "build/allocation_card.html" with item=item complete=False %} + {% endfor %} +
{% endblock %} {% block js_ready %} {{ block.super }} + {% for item in build.incomplete_outputs %} + + // Get the build output as a javascript object + inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, + { + success: function(response) { + loadBuildOutputAllocationTable( + {{ build.pk }}, + {{ build.part.pk }}, + response + ); + } + } + ); + {% endfor %} + var buildTable = $("#build-item-list"); // Calculate sum of allocations for a particular table row diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html new file mode 100644 index 0000000000..c00eaf6bbf --- /dev/null +++ b/InvenTree/build/templates/build/allocation_card.html @@ -0,0 +1,29 @@ +{% load i18n %} + +
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/InvenTree/build/templates/build/create_build_item.html b/InvenTree/build/templates/build/create_build_item.html index c08b987d36..8f58e884d6 100644 --- a/InvenTree/build/templates/build/create_build_item.html +++ b/InvenTree/build/templates/build/create_build_item.html @@ -1,9 +1,22 @@ {% extends "modal_form.html" %} +{% load i18n %} {% block pre_form_content %} +
+

+ {% trans "Select a stock item to allocate to the selected build output" %} +

+ {% if output %} +

+ {% trans "The allocated stock will be installed into the following build output:" %} +
+ {{ output }} +

+ {% endif %} +
{% if no_stock %} {% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index e8aeccab36..8303c88f9f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -492,23 +492,37 @@ class BuildItemDelete(AjaxDeleteView): class BuildItemCreate(AjaxCreateView): - """ View for allocating a new part to a build """ + """ + View for allocating a StockItems to a build output. + """ model = BuildItem form_class = forms.EditBuildItemForm ajax_template_name = 'build/create_build_item.html' - ajax_form_title = _('Allocate new Part') + ajax_form_title = _('Allocate stock to build output') role_required = 'build.add' + # The output StockItem against which the allocation is being made + output = None + + # The "part" which is being allocated to the output part = None + available_stock = None def get_context_data(self): - ctx = super(AjaxCreateView, self).get_context_data() + """ + Provide context data to the template which renders the form. + """ + + ctx = super().get_context_data() if self.part: ctx['part'] = self.part + if self.output: + ctx['output'] = self.output + if self.available_stock: ctx['stock'] = self.available_stock else: @@ -526,7 +540,28 @@ class BuildItemCreate(AjaxCreateView): build_id = form['build'].value() if build_id is not None: + """ + If the build has been provided, hide the widget to change the build selection. + Additionally, update the allowable selections for other fields. + """ form.fields['build'].widget = HiddenInput() + form.fields['install_into'].queryset = StockItem.objects.filter(build=build_id, is_building=True) + else: + """ + Build has *not* been selected + """ + pass + + # If the output stock item is specified, hide the input field + output_id = form['install_into'].value() + + if output_id is not None: + + try: + self.output = StockItem.objects.get(pk=output_id) + form.fields['install_into'].widget = HiddenInput() + except (ValueError, StockItem.DoesNotExist): + pass # If the sub_part is supplied, limit to matching stock items part_id = self.get_param('part') @@ -577,12 +612,15 @@ class BuildItemCreate(AjaxCreateView): """ Provide initial data for BomItem. Look for the folllowing in the GET data: - build: pk of the Build object + - part: pk of the Part object which we are assigning + - output: pk of the StockItem object into which the allocated stock will be installed """ initials = super(AjaxCreateView, self).get_initial().copy() build_id = self.get_param('build') part_id = self.get_param('part') + output_id = self.get_param('install_into') # Reference to a Part object part = None @@ -593,6 +631,9 @@ class BuildItemCreate(AjaxCreateView): # Reference to a Build object build = None + # Reference to a StockItem object + output = None + if part_id: try: part = Part.objects.get(pk=part_id) @@ -623,7 +664,7 @@ class BuildItemCreate(AjaxCreateView): if item_id: try: item = StockItem.objects.get(pk=item_id) - except: + except (ValueError, StockItem.DoesNotExist): pass # If a StockItem is not selected, try to auto-select one @@ -639,6 +680,17 @@ class BuildItemCreate(AjaxCreateView): else: quantity = min(quantity, item.unallocated_quantity()) + # If the output has been specified + print("output_id:", output_id) + if output_id: + try: + output = StockItem.objects.get(pk=output_id) + initials['install_into'] = output + print("Output:", output) + except (ValueError, StockItem.DoesNotExist): + pass + print("no output found") + if quantity is not None: initials['quantity'] = quantity diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d427409c71..b289b3e0ad 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -934,8 +934,10 @@ class Part(MPTTModel): def required_parts(self): """ Return a list of parts required to make this part (list of BOM items) """ parts = [] + for bom in self.bom_items.all().select_related('sub_part'): parts.append(bom.sub_part) + return parts def get_allowed_bom_items(self): diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 886657a2bc..4118e6ed54 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -29,9 +29,155 @@ function newBuildOrder(options={}) { ], } ) - } + +function loadBuildOutputAllocationTable(buildId, partId, output, options={}) { + /* + * Load the "allocation table" for a particular build output. + * + * Args: + * - buildId: The PK of the Build object + * - partId: The PK of the Part object + * - output: The StockItem object which is the "output" of the build + * - options: + * -- table: The #id of the table (will be auto-calculated if not provided) + */ + + var outputId = output.pk; + + var table = options.table || `#allocation-table-${outputId}`; + + function reloadTable() { + // Reload the entire build allocation table + $(table).bootstrapTable('refresh'); + } + + function setupCallbacks() { + // Register button callbacks once table data are loaded + + // Callback for 'allocate' button + $(table).find(".button-add").click(function() { + + // Primary key of the 'sub_part' + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + // Launch form to allocate new stock against this output + launchModalForm("{% url 'build-item-create' %}", { + success: reloadTable, + data: { + part: pk, + build: buildId, + install_into: outputId, + }, + secondary: [ + { + field: 'stock_item', + label: '{% trans "New Stock Item" %}', + title: '{% trans "Create new Stock Item" %}', + url: '{% url "stock-item-create" %}', + data: { + part: pk, + }, + }, + ] + }); + }); + } + + // Load table of BOM items + $(table).inventreeTable({ + url: "{% url 'api-bom-list' %}", + queryParams: { + part: partId, + sub_part_detail: true, + }, + formatNoMatches: function() { + return "{% trans "No BOM items found" %}"; + }, + name: 'build-allocation', + onPostBody: setupCallbacks, + onLoadSuccess: function(tableData) { + // Once the BOM data are loaded, request allocation data for this build output + + inventreeGet('/api/build/item/', + { + build: buildId, + output: outputId, + }, + { + success: function(data) { + // TODO + } + } + ); + }, + showColumns: false, + detailViewByClick: true, + detailView: true, + detailFilter: function(index, row) { + return row.allocations != null; + }, + detailFormatter: function(index, row, element) { + // TODO + return '---'; + }, + columns: [ + { + field: 'pk', + visible: false, + }, + { + field: 'sub_part_detail.full_name', + title: "{% trans "Required Part" %}", + sortable: true, + formatter: function(value, row, index, field) { + var url = `/part/${row.sub_part}/`; + var thumb = row.sub_part_detail.thumbnail; + var name = row.sub_part_detail.full_name; + + var html = imageHoverIcon(thumb) + renderLink(name, url); + + return html; + } + }, + { + field: 'reference', + title: '{% trans "Reference" %}', + }, + { + field: 'allocated', + title: '{% trans "Allocated" %}', + formatter: function(value, row, index, field) { + var allocated = value || 0; + var required = row.quantity * output.quantity; + + return makeProgressBar(allocated, required); + } + }, + { + field: 'actions', + title: '{% trans "Actions" %}', + formatter: function(value, row, index, field) { + // Generate action buttons for this build output + var html = `
`; + + html += makeIconButton('fa-plus icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}'); + + html += '
'; + + return html; + } + }, + ] + }); +} + + function loadBuildTable(table, options) { // Display a table of Build objects