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 %}
- No stock available for {{ part }}
+ {% trans "No stock available for" %} {{ part }}
{% 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