Add build output selection to builditem creation form

This commit is contained in:
Oliver Walters 2020-10-22 23:28:15 +11:00
parent 1ca08f8bff
commit fae516b38e
7 changed files with 284 additions and 10 deletions

View File

@ -8,6 +8,8 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
from django import forms from django import forms
from .models import Build, BuildItem from .models import Build, BuildItem
from stock.models import StockLocation from stock.models import StockLocation
@ -91,7 +93,7 @@ class CompleteBuildForm(HelperForm):
class CancelBuildForm(HelperForm): class CancelBuildForm(HelperForm):
""" Form for cancelling a build """ """ 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: class Meta:
model = Build model = Build
@ -101,7 +103,11 @@ class CancelBuildForm(HelperForm):
class EditBuildItemForm(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: class Meta:
model = BuildItem model = BuildItem
@ -109,4 +115,5 @@ class EditBuildItemForm(HelperForm):
'build', 'build',
'stock_item', 'stock_item',
'quantity', 'quantity',
'install_into',
] ]

View File

@ -20,14 +20,39 @@ InvenTree | Allocate Parts
</div> </div>
{% endif %} {% endif %}
</div> </div>
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table> <!--
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table>
-->
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
{% for item in build.incomplete_outputs %}
{% include "build/allocation_card.html" with item=item complete=False %}
{% endfor %}
</div>
{% endblock %} {% endblock %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ 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"); var buildTable = $("#build-item-list");
// Calculate sum of allocations for a particular table row // Calculate sum of allocations for a particular table row

View File

@ -0,0 +1,29 @@
{% load i18n %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="heading-{{ item.pk }}">
<div class="panel-title">
<div class='row'>
<div class='col-sm-6'>
<a role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-{{ item.pk }}" aria-expanded="true" aria-controls="collapse-{{ item.pk }}">
{{ item }}
</div>
</a>
<div class='col-sm-3'>
PROGRESS
</div>
<div class='col-sm-3'>
ACTIONS
<div class='btn-group float-right' id='#output-actions-{{ item.pk }}'>
<span class='fas fa-spin fa-spinner'></span>
</div>
</div>
</div>
</div>
</div>
<div id="collapse-{{ item.pk }}" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="heading-{{ item.pk }}">
<div class="panel-body">
<table class='table table-striped table-condensed' id='allocation-table-{{ item.pk }}'></table>
</div>
</div>
</div>

View File

@ -1,9 +1,22 @@
{% extends "modal_form.html" %} {% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %} {% block pre_form_content %}
<div class='alert alert-block alert-info'>
<p>
{% trans "Select a stock item to allocate to the selected build output" %}
</p>
{% if output %}
<p>
{% trans "The allocated stock will be installed into the following build output:" %}
<br>
<i>{{ output }}</i>
</p>
{% endif %}
</div>
{% if no_stock %} {% if no_stock %}
<div class='alert alert-danger alert-block' role='alert'> <div class='alert alert-danger alert-block' role='alert'>
No stock available for {{ part }} {% trans "No stock available for" %} {{ part }}
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -492,23 +492,37 @@ class BuildItemDelete(AjaxDeleteView):
class BuildItemCreate(AjaxCreateView): class BuildItemCreate(AjaxCreateView):
""" View for allocating a new part to a build """ """
View for allocating a StockItems to a build output.
"""
model = BuildItem model = BuildItem
form_class = forms.EditBuildItemForm form_class = forms.EditBuildItemForm
ajax_template_name = 'build/create_build_item.html' 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' 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 part = None
available_stock = None available_stock = None
def get_context_data(self): 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: if self.part:
ctx['part'] = self.part ctx['part'] = self.part
if self.output:
ctx['output'] = self.output
if self.available_stock: if self.available_stock:
ctx['stock'] = self.available_stock ctx['stock'] = self.available_stock
else: else:
@ -526,7 +540,28 @@ class BuildItemCreate(AjaxCreateView):
build_id = form['build'].value() build_id = form['build'].value()
if build_id is not None: 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['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 # If the sub_part is supplied, limit to matching stock items
part_id = self.get_param('part') 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: """ Provide initial data for BomItem. Look for the folllowing in the GET data:
- build: pk of the Build object - 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() initials = super(AjaxCreateView, self).get_initial().copy()
build_id = self.get_param('build') build_id = self.get_param('build')
part_id = self.get_param('part') part_id = self.get_param('part')
output_id = self.get_param('install_into')
# Reference to a Part object # Reference to a Part object
part = None part = None
@ -593,6 +631,9 @@ class BuildItemCreate(AjaxCreateView):
# Reference to a Build object # Reference to a Build object
build = None build = None
# Reference to a StockItem object
output = None
if part_id: if part_id:
try: try:
part = Part.objects.get(pk=part_id) part = Part.objects.get(pk=part_id)
@ -623,7 +664,7 @@ class BuildItemCreate(AjaxCreateView):
if item_id: if item_id:
try: try:
item = StockItem.objects.get(pk=item_id) item = StockItem.objects.get(pk=item_id)
except: except (ValueError, StockItem.DoesNotExist):
pass pass
# If a StockItem is not selected, try to auto-select one # If a StockItem is not selected, try to auto-select one
@ -639,6 +680,17 @@ class BuildItemCreate(AjaxCreateView):
else: else:
quantity = min(quantity, item.unallocated_quantity()) 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: if quantity is not None:
initials['quantity'] = quantity initials['quantity'] = quantity

View File

@ -934,8 +934,10 @@ class Part(MPTTModel):
def required_parts(self): def required_parts(self):
""" Return a list of parts required to make this part (list of BOM items) """ """ Return a list of parts required to make this part (list of BOM items) """
parts = [] parts = []
for bom in self.bom_items.all().select_related('sub_part'): for bom in self.bom_items.all().select_related('sub_part'):
parts.append(bom.sub_part) parts.append(bom.sub_part)
return parts return parts
def get_allowed_bom_items(self): def get_allowed_bom_items(self):

View File

@ -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 = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-plus icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}');
html += '</div>';
return html;
}
},
]
});
}
function loadBuildTable(table, options) { function loadBuildTable(table, options) {
// Display a table of Build objects // Display a table of Build objects