Working towards better build allocation:

- Improve data serialization in API
- Javascript bug fixes
- Load the build allocation table using jQuery
This commit is contained in:
Oliver Walters 2020-04-25 21:13:38 +10:00
parent c11b433d94
commit b0891c921c
10 changed files with 164 additions and 42 deletions

View File

@ -137,7 +137,4 @@ class BarcodePluginView(APIView):
# Include the original barcode data # Include the original barcode data
response['barcode_data'] = barcode_data response['barcode_data'] = barcode_data
print("Response:")
print(response)
return Response(response) return Response(response)

View File

@ -25,7 +25,6 @@ function inventreeGet(url, filters={}, options={}) {
dataType: 'json', dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
console.log('Success GET data at ' + url);
if (options.success) { if (options.success) {
options.success(response); options.success(response);
} }
@ -64,7 +63,6 @@ function inventreeFormDataUpload(url, data, options={}) {
processData: false, processData: false,
contentType: false, contentType: false,
success: function(data, status, xhr) { success: function(data, status, xhr) {
console.log('Form data upload success');
if (options.success) { if (options.success) {
options.success(data, status, xhr); options.success(data, status, xhr);
} }
@ -97,7 +95,6 @@ function inventreePut(url, data={}, options={}) {
dataType: 'json', dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
success: function(response, status) { success: function(response, status) {
console.log(method + ' - ' + url + ' : result = ' + status);
if (options.success) { if (options.success) {
options.success(response, status); options.success(response, status);
} }
@ -114,25 +111,3 @@ function inventreePut(url, data={}, options={}) {
} }
}); });
} }
// Return list of parts with optional filters
function getParts(filters={}, options={}) {
return inventreeGet('/api/part/', filters, options);
}
// Return list of part categories with optional filters
function getPartCategories(filters={}, options={}) {
return inventreeGet('/api/part/category/', filters, options);
}
function getCompanies(filters={}, options={}) {
return inventreeGet('/api/company/', filters, options);
}
function updateStockItem(pk, data, final=false) {
return inventreePut('/api/stock/' + pk + '/', data, final);
}
function updatePart(pk, data, final=false) {
return inventreePut('/api/part/' + pk + '/', data, final);
}

View File

@ -1,4 +1,5 @@
function loadBuildTable(table, options) { function loadBuildTable(table, options) {
// Display a table of Build objects
var params = options.params || {}; var params = options.params || {};

View File

@ -121,7 +121,7 @@ function makeProgressBar(value, maximum, opts) {
extraclass = 'progress-bar-under'; extraclass = 'progress-bar-under';
} }
var id = opts.id || 'progress-bar'; var id = options.id || 'progress-bar';
return ` return `
<div id='${id}' class='progress'> <div id='${id}' class='progress'>

View File

@ -95,20 +95,25 @@ class BuildItemList(generics.ListCreateAPIView):
to allow filtering by stock_item.part to allow filtering by stock_item.part
""" """
# Does the user wish to filter by part?
part_pk = self.request.query_params.get('part', None)
query = BuildItem.objects.all() query = BuildItem.objects.all()
query = query.select_related('stock_item') query = query.select_related('stock_item')
query = query.prefetch_related('stock_item__part') query = query.prefetch_related('stock_item__part')
query = query.prefetch_related('stock_item__part__category') query = query.prefetch_related('stock_item__part__category')
if part_pk:
query = query.filter(stock_item__part=part_pk)
return query return query
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# Does the user wish to filter by part?
part_pk = self.request.query_params.get('part', None)
if part_pk:
queryset = queryset.filter(stock_item__part=part_pk)
return queryset
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
] ]
@ -128,7 +133,7 @@ build_item_api_urls = [
] ]
build_api_urls = [ build_api_urls = [
url(r'^item/?', include(build_item_api_urls)), url(r'^item/', include(build_item_api_urls)),
url(r'^(?P<pk>\d+)/', BuildDetail.as_view(), name='api-build-detail'), url(r'^(?P<pk>\d+)/', BuildDetail.as_view(), name='api-build-detail'),

View File

@ -21,6 +21,8 @@ class BuildSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
quantity = serializers.FloatField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False) part_detail = kwargs.pop('part_detail', False)
@ -63,6 +65,8 @@ class BuildItemSerializer(InvenTreeModelSerializer):
part_image = serializers.CharField(source='stock_item.part.image', read_only=True) part_image = serializers.CharField(source='stock_item.part.image', read_only=True)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
quantity = serializers.FloatField()
class Meta: class Meta:
model = BuildItem model = BuildItem
fields = [ fields = [

View File

@ -1,5 +1,6 @@
{% extends "build/build_base.html" %} {% extends "build/build_base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
{% block page_title %} {% block page_title %}
@ -27,6 +28,141 @@ InvenTree | Allocate Parts
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
var buildTable = $("#build-item-list");
buildTable.inventreeTable({
uniqueId: 'sub_part',
url: "{% url 'api-bom-list' %}",
formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; },
onLoadSuccess: function(tableData) {
// Once the BOM data are loaded, request allocation data for the build
inventreeGet('/api/build/item/', {
build: {{ build.id }},
},
{
success: function(data) {
// Iterate through the returned data, and group by "part"
var allocations = {};
data.forEach(function(item) {
// Group allocations by referenced 'part'
var part = item.part;
var key = parseInt(part);
if (!(key in allocations)) {
allocations[key] = new Array();
}
// Add the allocation to the list
allocations[key].push(item);
});
for (var key in allocations) {
// Select the associated row in the table
var tableRow = buildTable.bootstrapTable('getRowByUniqueId', key);
// Set the allocations for the row
tableRow.allocations = allocations[key];
// And push the updated row back into the main table
buildTable.bootstrapTable('updateByUniqueId', key, tableRow, true);
}
}
},
);
},
queryParams: {
part: {{ build.part.id }},
sub_part_detail: 1,
},
columns: [
{
field: 'id',
visible: false,
},
{
sortable: true,
field: 'sub_part',
title: '{% trans "Part" %}',
formatter: function(value, row, index, field) {
return imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, `/part/${row.sub_part}/`);
},
},
{
sortable: true,
field: 'sub_part_detail.description',
title: '{% trans "Description" %}',
},
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Required" %}',
formatter: function(value, row) {
return value * {{ build.quantity }};
},
},
{
sortable: true,
field: 'allocated',
title: '{% trans "Allocated" %}',
formatter: function(value, row) {
var progress = value || 0;
return makeProgressBar(progress, row.quantity * {{ build.quantity }});
},
sorter: function(valA, valB, rowA, rowB) {
var aA = rowA.allocated || 0;
var aB = rowB.allocated || 0;
var qA = rowA.quantity * {{ build.quantity }};
var qB = rowB.quantity * {{ build.quantity }};
if (aA == 0 && aB == 0) {
return (qA > qB) ? 1 : -1;
}
var progressA = parseFloat(aA) / qA;
var progressB = parseFloat(aB) / qB;
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'buttons',
formatter: function(value, row, index, field) {
var html = `<div class='btn-group float-right' role='group'>`;
var pk = row.sub_part;
{% if build.status == BuildStatus.PENDING %}
if (row.sub_part_detail.purchaseable) {
html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}');
}
if (row.sub_part.assembly) {
html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}');
}
html += makeIconButton('fa-plus', 'button-add', pk, '{% trans "Allocate stock" %}');
{% endif %}
html += '</div>';
return html;
},
}
],
});
{% if editing %} {% if editing %}
{% for bom_item in bom_items.all %} {% for bom_item in bom_items.all %}

View File

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
<h4>{% trans "Required Parts" %}</h4> <h4>{% trans "Allocated Parts" %}</h4>
<hr> <hr>
<div id='build-item-toolbar'> <div id='build-item-toolbar'>
@ -11,7 +11,9 @@
</div> </div>
</div> </div>
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'> <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-list' data-sorting='true'>
<thead> <thead>
<tr> <tr>
<th data-sortable='true'>{% trans "Part" %}</th> <th data-sortable='true'>{% trans "Part" %}</th>

View File

@ -4,13 +4,13 @@
<li{% if tab == 'details' %} class='active'{% endif %}> <li{% if tab == 'details' %} class='active'{% endif %}>
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a> <a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
</li> </li>
<li{% if tab == 'allocate' %} class='active'{% endif %}>
<a href="{% url 'build-allocate' build.id %}">{% trans "Allocated Parts" %}</a>
</li>
<li{% if tab == 'output' %} class='active'{% endif %}> <li{% if tab == 'output' %} class='active'{% endif %}>
<a href="{% url 'build-output' build.id %}">{% trans "Outputs" %}{% if build.output_count > 0%}<span class='badge'>{{ build.output_count }}</span>{% endif %}</a> <a href="{% url 'build-output' build.id %}">{% trans "Build Outputs" %}{% if build.output_count > 0%}<span class='badge'>{{ build.output_count }}</span>{% endif %}</a>
</li> </li>
<li{% if tab == 'notes' %} class='active'{% endif %}> <li{% if tab == 'notes' %} class='active'{% endif %}>
<a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a> <a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li> </li>
<li{% if tab == 'allocate' %} class='active'{% endif %}>
<a href="{% url 'build-allocate' build.id %}">{% trans "Assign Parts" %}</a>
</li>
</ul> </ul>

View File

@ -235,6 +235,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
price_range = serializers.CharField(read_only=True) price_range = serializers.CharField(read_only=True)
quantity = serializers.FloatField()
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)