diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
index 33f3f4ab36..a720f7cbe0 100644
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -96,6 +96,7 @@ class BuildList(generics.ListCreateAPIView):
'target_date',
'completion_date',
'quantity',
+ 'completed',
'issued_by',
'responsible',
]
@@ -442,6 +443,18 @@ class BuildItemList(generics.ListCreateAPIView):
if part_pk:
queryset = queryset.filter(stock_item__part=part_pk)
+ # Filter by "tracked" status
+ # Tracked means that the item is "installed" into a build output (stock item)
+ tracked = params.get('tracked', None)
+
+ if tracked is not None:
+ tracked = str2bool(tracked)
+
+ if tracked:
+ queryset = queryset.exclude(install_into=None)
+ else:
+ queryset = queryset.filter(install_into=None)
+
# Filter by output target
output = params.get('output', None)
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index e5189e6073..86bb256539 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -1260,7 +1260,7 @@ class BuildItem(models.Model):
})
@transaction.atomic
- def complete_allocation(self, user):
+ def complete_allocation(self, user, notes=''):
"""
Complete the allocation of this BuildItem into the output stock item.
@@ -1286,8 +1286,13 @@ class BuildItem(models.Model):
self.save()
# Install the stock item into the output
- item.belongs_to = self.install_into
- item.save()
+ self.install_into.installStockItem(
+ item,
+ self.quantity,
+ user,
+ notes
+ )
+
else:
# Simply remove the items from stock
item.take_stock(
diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py
index 4b55182563..d037ad546e 100644
--- a/InvenTree/build/serializers.py
+++ b/InvenTree/build/serializers.py
@@ -161,7 +161,12 @@ class BuildOutputSerializer(serializers.Serializer):
# The build output must have all tracked parts allocated
if not build.is_fully_allocated(output):
- raise ValidationError(_("This build output is not fully allocated"))
+
+ # Check if the user has specified that incomplete allocations are ok
+ accept_incomplete = InvenTree.helpers.str2bool(self.context['request'].data.get('accept_incomplete_allocation', False))
+
+ if not accept_incomplete:
+ raise ValidationError(_("This build output is not fully allocated"))
return output
@@ -355,6 +360,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
'outputs',
'location',
'status',
+ 'accept_incomplete_allocation',
'notes',
]
@@ -377,6 +383,13 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
label=_("Status"),
)
+ accept_incomplete_allocation = serializers.BooleanField(
+ default=False,
+ required=False,
+ label=_('Accept Incomplete Allocation'),
+ help_text=_('Complete ouputs if stock has not been fully allocated'),
+ )
+
notes = serializers.CharField(
label=_("Notes"),
required=False,
@@ -617,6 +630,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
super().validate(data)
+ build = self.context['build']
bom_item = data['bom_item']
stock_item = data['stock_item']
quantity = data['quantity']
@@ -641,16 +655,20 @@ class BuildAllocationItemSerializer(serializers.Serializer):
# Output *must* be set for trackable parts
if output is None and bom_item.sub_part.trackable:
raise ValidationError({
- 'output': _('Build output must be specified for allocation of tracked parts')
+ 'output': _('Build output must be specified for allocation of tracked parts'),
})
# Output *cannot* be set for un-tracked parts
if output is not None and not bom_item.sub_part.trackable:
raise ValidationError({
- 'output': _('Build output cannot be specified for allocation of untracked parts')
+ 'output': _('Build output cannot be specified for allocation of untracked parts'),
})
+ # Check if this allocation would be unique
+ if BuildItem.objects.filter(build=build, stock_item=stock_item, install_into=output).exists():
+ raise ValidationError(_('This stock item has already been allocated to this build output'))
+
return data
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index 92e1177e0f..42bc51bb2f 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -270,6 +270,16 @@
+ {% if build.has_tracked_bom_items %}
+
+
+
+ {% endif %}
+
{% include "filter_list.html" with id='incompletebuilditems' %}
{% endif %}
@@ -401,110 +411,53 @@ function reloadTable() {
$('#allocation-table-untracked').bootstrapTable('refresh');
}
-// Get the list of BOM items required for this build
-inventreeGet(
- '{% url "api-bom-list" %}',
- {
+onPanelLoad('outputs', function() {
+ {% if build.active %}
+
+ var build_info = {
+ pk: {{ build.pk }},
part: {{ build.part.pk }},
- sub_part_detail: true,
- },
- {
- success: function(response) {
+ quantity: {{ build.quantity }},
+ {% if build.take_from %}
+ source_location: {{ build.take_from.pk }},
+ {% endif %}
+ tracked_parts: true,
+ };
- var build_info = {
- pk: {{ build.pk }},
- part: {{ build.part.pk }},
- quantity: {{ build.quantity }},
- bom_items: response,
- {% if build.take_from %}
- source_location: {{ build.take_from.pk }},
- {% endif %}
- {% if build.has_tracked_bom_items %}
- tracked_parts: true,
- {% else %}
- tracked_parts: false,
- {% endif %}
- };
+ loadBuildOutputTable(build_info);
- {% if build.active %}
- loadBuildOutputTable(build_info);
- linkButtonsToSelection(
- '#build-output-table',
- [
- '#output-options',
- '#multi-output-complete',
- '#multi-output-delete',
- ]
- );
+ {% endif %}
+});
- $('#multi-output-complete').click(function() {
- var outputs = $('#build-output-table').bootstrapTable('getSelections');
+{% if build.active and build.has_untracked_bom_items %}
- completeBuildOutputs(
- build_info.pk,
- outputs,
- {
- success: function() {
- // Reload the "in progress" table
- $('#build-output-table').bootstrapTable('refresh');
+function loadUntrackedStockTable() {
- // Reload the "completed" table
- $('#build-stock-table').bootstrapTable('refresh');
- }
- }
- );
- });
+ var build_info = {
+ pk: {{ build.pk }},
+ part: {{ build.part.pk }},
+ quantity: {{ build.quantity }},
+ {% if build.take_from %}
+ source_location: {{ build.take_from.pk }},
+ {% endif %}
+ tracked_parts: false,
+ };
+
+ $('#allocation-table-untracked').bootstrapTable('destroy');
- $('#multi-output-delete').click(function() {
- var outputs = $('#build-output-table').bootstrapTable('getSelections');
-
- deleteBuildOutputs(
- build_info.pk,
- outputs,
- {
- success: function() {
- // Reload the "in progress" table
- $('#build-output-table').bootstrapTable('refresh');
-
- // Reload the "completed" table
- $('#build-stock-table').bootstrapTable('refresh');
- }
- }
- )
- });
-
- $('#incomplete-output-print-label').click(function() {
- var outputs = $('#build-output-table').bootstrapTable('getSelections');
-
- if (outputs.length == 0) {
- outputs = $('#build-output-table').bootstrapTable('getData');
- }
-
- var stock_id_values = [];
-
- outputs.forEach(function(output) {
- stock_id_values.push(output.pk);
- });
-
- printStockItemLabels(stock_id_values);
-
- });
-
- {% endif %}
-
- {% if build.active and build.has_untracked_bom_items %}
- // Load allocation table for un-tracked parts
- loadBuildOutputAllocationTable(
- build_info,
- null,
- {
- search: true,
- }
- );
- {% endif %}
+ // Load allocation table for un-tracked parts
+ loadBuildOutputAllocationTable(
+ build_info,
+ null,
+ {
+ search: true,
}
- }
-);
+ );
+}
+
+loadUntrackedStockTable();
+
+{% endif %}
$('#btn-create-output').click(function() {
@@ -527,6 +480,7 @@ $("#btn-auto-allocate").on('click', function() {
{% if build.take_from %}
location: {{ build.take_from.pk }},
{% endif %}
+ onSuccess: loadUntrackedStockTable,
}
);
});
@@ -558,9 +512,7 @@ $("#btn-allocate").on('click', function() {
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
- success: function(data) {
- $('#allocation-table-untracked').bootstrapTable('refresh');
- }
+ success: loadUntrackedStockTable,
}
);
}
@@ -569,6 +521,7 @@ $("#btn-allocate").on('click', function() {
$('#btn-unallocate').on('click', function() {
unallocateStock({{ build.id }}, {
table: '#allocation-table-untracked',
+ onSuccess: loadUntrackedStockTable,
});
});
@@ -588,9 +541,7 @@ $('#allocate-selected-items').click(function() {
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
- success: function(data) {
- $('#allocation-table-untracked').bootstrapTable('refresh');
- }
+ success: loadUntrackedStockTable,
}
);
});
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 1a80c87322..3752f7daf4 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -383,7 +383,7 @@ class PartTestTemplateList(generics.ListCreateAPIView):
required = params.get('required', None)
if required is not None:
- queryset = queryset.filter(required=required)
+ queryset = queryset.filter(required=str2bool(required))
return queryset
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index d4fc5c93d1..c88c29e64e 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -23,6 +23,8 @@ from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework import generics, filters
+from build.models import Build
+
import common.settings
import common.models
@@ -1159,6 +1161,19 @@ class StockItemTestResultList(generics.ListCreateAPIView):
queryset = super().filter_queryset(queryset)
+ # Filter by 'build'
+ build = params.get('build', None)
+
+ if build is not None:
+
+ try:
+ build = Build.objects.get(pk=build)
+
+ queryset = queryset.filter(stock_item__build=build)
+
+ except (ValueError, Build.DoesNotExist):
+ pass
+
# Filter by stock item
item = params.get('stock_item', None)
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 2d7796edcd..e4674d5989 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -743,11 +743,29 @@ function loadBomTable(table, options={}) {
field: 'sub_part',
title: '{% trans "Part" %}',
sortable: true,
+ switchable: false,
formatter: function(value, row) {
var url = `/part/${row.sub_part}/`;
- var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
+ var html = '';
var sub_part = row.sub_part_detail;
+
+ // Display an extra icon if this part is an assembly
+ if (sub_part.assembly) {
+
+ if (row.sub_assembly_received) {
+ // Data received, ignore
+ } else if (row.sub_assembly_requested) {
+ html += ``;
+ } else {
+ html += `
+
+
+ `;
+ }
+ }
+
+ html += imageHoverIcon(sub_part.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
html += makePartIcons(sub_part);
@@ -759,13 +777,6 @@ function loadBomTable(table, options={}) {
html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}');
}
- // Display an extra icon if this part is an assembly
- if (sub_part.assembly) {
- var text = ``;
-
- html += renderLink(text, `/part/${row.sub_part}/bom/`);
- }
-
return html;
}
}
@@ -1027,14 +1038,6 @@ function loadBomTable(table, options={}) {
// This function may be called recursively for multi-level BOMs
function requestSubItems(bom_pk, part_pk, depth=0) {
- // Prevent multi-level recursion
- const MAX_BOM_DEPTH = 25;
-
- if (depth >= MAX_BOM_DEPTH) {
- console.log(`Maximum BOM depth (${MAX_BOM_DEPTH}) reached!`);
- return;
- }
-
inventreeGet(
options.bom_url,
{
@@ -1049,17 +1052,13 @@ function loadBomTable(table, options={}) {
for (var idx = 0; idx < response.length; idx++) {
response[idx].parentId = bom_pk;
}
+
+ var row = $(table).bootstrapTable('getRowByUniqueId', bom_pk);
+ row.sub_assembly_received = true;
+
+ $(table).bootstrapTable('updateByUniqueId', bom_pk, row, true);
table.bootstrapTable('append', response);
-
- // Next, re-iterate and check if the new items also have sub items
- response.forEach(function(bom_item) {
- if (bom_item.sub_part_detail.assembly) {
- requestSubItems(bom_item.pk, bom_item.sub_part, depth + 1);
- }
- });
-
- table.treegrid('collapseAll');
},
error: function(xhr) {
console.log('Error requesting BOM for part=' + part_pk);
@@ -1103,7 +1102,6 @@ function loadBomTable(table, options={}) {
formatNoMatches: function() {
return '{% trans "No BOM items found" %}';
},
- clickToSelect: true,
queryParams: filters,
original: params,
columns: cols,
@@ -1117,32 +1115,26 @@ function loadBomTable(table, options={}) {
});
table.treegrid('collapseAll');
+
+ // Callback for 'load sub assembly' button
+ $(table).find('.load-sub-assembly').click(function(event) {
+
+ event.preventDefault();
+
+ var pk = $(this).attr('pk');
+ var row = $(table).bootstrapTable('getRowByUniqueId', pk);
+
+ // Request BOM data for this subassembly
+ requestSubItems(row.pk, row.sub_part);
+
+ row.sub_assembly_requested = true;
+ $(table).bootstrapTable('updateByUniqueId', pk, row, true);
+ });
},
onLoadSuccess: function() {
-
if (options.editable) {
table.bootstrapTable('uncheckAll');
}
-
- var data = table.bootstrapTable('getData');
-
- for (var idx = 0; idx < data.length; idx++) {
- var row = data[idx];
-
- // If a row already has a parent ID set, it's already been updated!
- if (row.parentId) {
- continue;
- }
-
- // Set the parent ID of the top-level rows
- row.parentId = parent_id;
-
- table.bootstrapTable('updateRow', idx, row, true);
-
- if (row.sub_part_detail.assembly) {
- requestSubItems(row.pk, row.sub_part);
- }
- }
},
});
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index 65fc3a4d6c..c9ebbe0e22 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -264,7 +264,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
var html = `
`;
// Tracked parts? Must be individually allocated
- if (build_info.tracked_parts) {
+ if (options.has_bom_items) {
// Add a button to allocate stock against this build output
html += makeIconButton(
@@ -342,7 +342,9 @@ function unallocateStock(build_id, options={}) {
},
title: '{% trans "Unallocate Stock Items" %}',
onSuccess: function(response, opts) {
- if (options.table) {
+ if (options.onSuccess) {
+ options.onSuccess(response, opts);
+ } else if (options.table) {
// Reload the parent table
$(options.table).bootstrapTable('refresh');
}
@@ -427,6 +429,8 @@ function completeBuildOutputs(build_id, outputs, options={}) {
fields: {
status: {},
location: {},
+ notes: {},
+ accept_incomplete_allocation: {},
},
confirm: true,
title: '{% trans "Complete Build Outputs" %}',
@@ -445,6 +449,8 @@ function completeBuildOutputs(build_id, outputs, options={}) {
outputs: [],
status: getFormFieldValue('status', {}, opts),
location: getFormFieldValue('location', {}, opts),
+ notes: getFormFieldValue('notes', {}, opts),
+ accept_incomplete_allocation: getFormFieldValue('accept_incomplete_allocation', {type: 'boolean'}, opts),
};
var output_pk_values = [];
@@ -720,6 +726,35 @@ function loadBuildOrderAllocationTable(table, options={}) {
}
+/* Internal helper functions for performing calculations on BOM data */
+
+// Iterate through a list of allocations, returning *only* those which match a particular BOM row
+function getAllocationsForBomRow(bom_row, allocations) {
+ var part_id = bom_row.sub_part;
+
+ var matching_allocations = [];
+
+ allocations.forEach(function(allocation) {
+ if (allocation.bom_part == part_id) {
+ matching_allocations.push(allocation);
+ }
+ });
+
+ return matching_allocations;
+}
+
+// Sum the allocation quantity for a given BOM row
+function sumAllocationsForBomRow(bom_row, allocations) {
+ var quantity = 0;
+
+ getAllocationsForBomRow(bom_row, allocations).forEach(function(allocation) {
+ quantity += allocation.quantity;
+ });
+
+ return parseFloat(quantity).toFixed(15);
+}
+
+
/*
* Display a "build output" table for a particular build.
*
@@ -737,18 +772,6 @@ function loadBuildOutputTable(build_info, options={}) {
params.is_building = true;
params.build = build_info.pk;
- // Construct a list of "tracked" BOM items
- var tracked_bom_items = [];
-
- var has_tracked_items = false;
-
- build_info.bom_items.forEach(function(bom_item) {
- if (bom_item.sub_part_detail.trackable) {
- tracked_bom_items.push(bom_item);
- has_tracked_items = true;
- };
- });
-
var filters = {};
for (var key in params) {
@@ -786,7 +809,7 @@ function loadBuildOutputTable(build_info, options={}) {
}
);
} else {
- console.log(`WARNING: Could not locate sub-table for output ${pk}`);
+ console.warn(`Could not locate sub-table for output ${pk}`);
}
});
@@ -841,6 +864,26 @@ function loadBuildOutputTable(build_info, options={}) {
});
}
+ // List of "tracked bom items" required for this build order
+ var bom_items = null;
+
+ // Request list of BOM data for this build order
+ inventreeGet(
+ '{% url "api-bom-list" %}',
+ {
+ part: build_info.part,
+ sub_part_detail: true,
+ sub_part_trackable: true,
+ },
+ {
+ async: false,
+ success: function(response) {
+ // Save the BOM items
+ bom_items = response;
+ }
+ }
+ );
+
/*
* Construct a "sub table" showing the required BOM items
*/
@@ -855,6 +898,9 @@ function loadBuildOutputTable(build_info, options={}) {
element.html(html);
+ // Pass through the cached BOM items
+ build_info.bom_items = bom_items;
+
loadBuildOutputAllocationTable(
build_info,
row,
@@ -865,19 +911,180 @@ function loadBuildOutputTable(build_info, options={}) {
);
}
+ function updateAllocationData(rows) {
+ // Update stock allocation information for the build outputs
+
+ // Request updated stock allocation data for this build order
+ inventreeGet(
+ '{% url "api-build-item-list" %}',
+ {
+ build: build_info.pk,
+ part_detail: true,
+ location_detail: true,
+ sub_part_trackable: true,
+ tracked: true,
+ },
+ {
+ success: function(response) {
+
+ // Group allocation information by the "install_into" field
+ var allocations = {};
+
+ response.forEach(function(allocation) {
+ var target = allocation.install_into;
+
+ if (target != null) {
+ if (!(target in allocations)) {
+ allocations[target] = [];
+ }
+
+ allocations[target].push(allocation);
+ }
+ });
+
+ // Now that the allocations have been grouped by stock item,
+ // we can update each row in the table,
+ // using the pk value of each row (stock item)
+ rows.forEach(function(row) {
+ row.allocations = allocations[row.pk] || [];
+ $(table).bootstrapTable('updateByUniqueId', row.pk, row, true);
+
+ var n_completed_lines = 0;
+
+ // Check how many BOM lines have been completely allocated for this build output
+ bom_items.forEach(function(bom_item) {
+
+ var required_quantity = bom_item.quantity * row.quantity;
+
+ if (sumAllocationsForBomRow(bom_item, row.allocations) >= required_quantity) {
+ n_completed_lines += 1;
+ }
+
+ var output_progress_bar = $(`#output-progress-${row.pk}`);
+
+ if (output_progress_bar.exists()) {
+ output_progress_bar.html(
+ makeProgressBar(
+ n_completed_lines,
+ bom_items.length,
+ {
+ max_width: '150px',
+ }
+ )
+ );
+ }
+ });
+ });
+ }
+ }
+ );
+ }
+
+ var part_tests = null;
+
+ function updateTestResultData(rows) {
+ // Update test result information for the build outputs
+
+ // Request test template data if it has not already been retrieved
+ if (part_tests == null) {
+ inventreeGet(
+ '{% url "api-part-test-template-list" %}',
+ {
+ part: build_info.part,
+ required: true,
+ },
+ {
+ success: function(response) {
+ // Save the list of part tests
+ part_tests = response;
+
+ // Callback to this function again
+ updateTestResultData(rows);
+ }
+ }
+ );
+
+ return;
+ }
+
+ // Retrieve stock results for the entire build
+ inventreeGet(
+ '{% url "api-stock-test-result-list" %}',
+ {
+ build: build_info.pk,
+ },
+ {
+ success: function(results) {
+
+ // Iterate through each row and find matching test results
+ rows.forEach(function(row) {
+ var test_results = {};
+
+ results.forEach(function(result) {
+ if (result.stock_item == row.pk) {
+ // This test result matches the particular stock item
+
+ if (!(result.key in test_results)) {
+ test_results[result.key] = result.result;
+ }
+ }
+ });
+
+ row.passed_tests = test_results;
+
+ $(table).bootstrapTable('updateByUniqueId', row.pk, row, true);
+ });
+ }
+ }
+ );
+ }
+
+ // Return the number of 'passed' tests in a given row
+ function countPassedTests(row) {
+ if (part_tests == null) {
+ return 0;
+ }
+
+ var results = row.passed_tests || {};
+ var n = 0;
+
+ part_tests.forEach(function(test) {
+ if (results[test.key] || false) {
+ n += 1;
+ }
+ });
+
+ return n;
+ }
+
+ // Return the number of 'fully allocated' lines for a given row
+ function countAllocatedLines(row) {
+ var n_completed_lines = 0;
+
+ bom_items.forEach(function(bom_row) {
+ var required_quantity = bom_row.quantity * row.quantity;
+
+ if (sumAllocationsForBomRow(bom_row, row.allocations || []) >= required_quantity) {
+ n_completed_lines += 1;
+ }
+ });
+
+ return n_completed_lines;
+ }
+
$(table).inventreeTable({
url: '{% url "api-stock-list" %}',
queryParams: filters,
original: params,
- showColumns: false,
+ showColumns: true,
uniqueId: 'pk',
name: 'build-outputs',
sortable: true,
search: false,
- sidePagination: 'server',
- detailView: has_tracked_items,
+ sidePagination: 'client',
+ detailView: bom_items.length > 0,
detailFilter: function(index, row) {
- return true;
+ return bom_items.length > 0;
},
detailFormatter: function(index, row, element) {
constructBuildOutputSubTable(index, row, element);
@@ -885,11 +1092,14 @@ function loadBuildOutputTable(build_info, options={}) {
formatNoMatches: function() {
return '{% trans "No active build outputs found" %}';
},
- onPostBody: function() {
+ onPostBody: function(rows) {
// Add callbacks for the buttons
setupBuildOutputButtonCallbacks();
+ },
+ onLoadSuccess: function(rows) {
- $(table).bootstrapTable('expandAllRows');
+ updateAllocationData(rows);
+ updateTestResultData(rows);
},
columns: [
{
@@ -901,6 +1111,7 @@ function loadBuildOutputTable(build_info, options={}) {
{
field: 'part',
title: '{% trans "Part" %}',
+ switchable: true,
formatter: function(value, row) {
var thumb = row.part_detail.thumbnail;
@@ -909,7 +1120,9 @@ function loadBuildOutputTable(build_info, options={}) {
},
{
field: 'quantity',
- title: '{% trans "Quantity" %}',
+ title: '{% trans "Build Output" %}',
+ switchable: false,
+ sortable: true,
formatter: function(value, row) {
var url = `/stock/item/${row.pk}/`;
@@ -922,15 +1135,84 @@ function loadBuildOutputTable(build_info, options={}) {
text = `{% trans "Quantity" %}: ${row.quantity}`;
}
+ if (row.batch) {
+ text += `
({% trans "Batch" %}: ${row.batch})`;
+ }
+
return renderLink(text, url);
+ },
+ sorter: function(a, b, row_a, row_b) {
+ // Sort first by quantity, and then by serial number
+ if ((row_a.quantity > 1) || (row_b.quantity > 1)) {
+ return row_a.quantity > row_b.quantity ? 1 : -1;
+ }
+
+ if ((row_a.serial != null) && (row_b.serial != null)) {
+ var sn_a = Number.parseInt(row_a.serial) || 0;
+ var sn_b = Number.parseInt(row_b.serial) || 0;
+
+ return sn_a > sn_b ? 1 : -1;
+ }
+
+ return 0;
}
},
{
field: 'allocated',
- title: '{% trans "Allocated Parts" %}',
- visible: has_tracked_items,
+ title: '{% trans "Allocated Stock" %}',
+ visible: bom_items.length > 0,
+ switchable: false,
+ sortable: true,
formatter: function(value, row) {
- return `
`;
+
+ if (bom_items.length == 0) {
+ return `
{% trans "No tracked BOM items for this build" %}
`;
+ }
+
+ var progressBar = makeProgressBar(
+ countAllocatedLines(row),
+ bom_items.length,
+ {
+ max_width: '150px',
+ }
+ );
+
+ return `
${progressBar}
`;
+ },
+ sorter: function(value_a, value_b, row_a, row_b) {
+ var q_a = countAllocatedLines(row_a);
+ var q_b = countAllocatedLines(row_b);
+
+ return q_a > q_b ? 1 : -1;
+ },
+ },
+ {
+ field: 'tests',
+ title: '{% trans "Completed Tests" %}',
+ sortable: true,
+ switchable: true,
+ formatter: function(value, row) {
+ if (part_tests == null || part_tests.length == 0) {
+ return `
{% trans "No required tests for this build" %}`;
+ }
+
+ var n_passed = countPassedTests(row);
+
+ var progress = makeProgressBar(
+ n_passed,
+ part_tests.length,
+ {
+ max_width: '150px',
+ }
+ );
+
+ return progress;
+ },
+ sorter: function(a, b, row_a, row_b) {
+ var n_a = countPassedTests(row_a);
+ var n_b = countPassedTests(row_b);
+
+ return n_a > n_b ? 1 : -1;
}
},
{
@@ -941,6 +1223,9 @@ function loadBuildOutputTable(build_info, options={}) {
return makeBuildOutputButtons(
row.pk,
build_info,
+ {
+ has_bom_items: bom_items.length > 0,
+ }
);
}
}
@@ -956,6 +1241,79 @@ function loadBuildOutputTable(build_info, options={}) {
$(table).on('collapse-row.bs.table', function(detail, index, row) {
$(`#button-output-allocate-${row.pk}`).prop('disabled', true);
});
+
+ // Add callbacks for the various table menubar buttons
+
+ // Complete multiple outputs
+ $('#multi-output-complete').click(function() {
+ var outputs = $(table).bootstrapTable('getSelections');
+
+ if (outputs.length == 0) {
+ outputs = $(table).bootstrapTable('getData');
+ }
+
+ completeBuildOutputs(
+ build_info.pk,
+ outputs,
+ {
+ success: function() {
+ // Reload the "in progress" table
+ $('#build-output-table').bootstrapTable('refresh');
+
+ // Reload the "completed" table
+ $('#build-stock-table').bootstrapTable('refresh');
+ }
+ }
+ );
+ });
+
+ // Delete multiple build outputs
+ $('#multi-output-delete').click(function() {
+ var outputs = $(table).bootstrapTable('getSelections');
+
+ if (outputs.length == 0) {
+ outputs = $(table).bootstrapTable('getData');
+ }
+
+ deleteBuildOutputs(
+ build_info.pk,
+ outputs,
+ {
+ success: function() {
+ // Reload the "in progress" table
+ $('#build-output-table').bootstrapTable('refresh');
+
+ // Reload the "completed" table
+ $('#build-stock-table').bootstrapTable('refresh');
+ }
+ }
+ );
+ });
+
+ // Print stock item labels
+ $('#incomplete-output-print-label').click(function() {
+ var outputs = $(table).bootstrapTable('getSelections');
+
+ if (outputs.length == 0) {
+ outputs = $(table).bootstrapTable('getData');
+ }
+
+ var stock_id_values = [];
+
+ outputs.forEach(function(output) {
+ stock_id_values.push(output.pk);
+ });
+
+ printStockItemLabels(stock_id_values);
+ });
+
+ $('#outputs-expand').click(function() {
+ $(table).bootstrapTable('expandAllRows');
+ });
+
+ $('#outputs-collapse').click(function() {
+ $(table).bootstrapTable('collapseAllRows');
+ });
}
@@ -973,7 +1331,6 @@ function loadBuildOutputTable(build_info, options={}) {
*/
function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
-
var buildId = buildInfo.pk;
var partId = buildInfo.part;
@@ -985,6 +1342,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
outputId = 'untracked';
}
+ var bom_items = buildInfo.bom_items || null;
+
+ // If BOM items have not been provided, load via the API
+ if (bom_items == null) {
+ inventreeGet(
+ '{% url "api-bom-list" %}',
+ {
+ part: partId,
+ sub_part_detail: true,
+ sub_part_trackable: buildInfo.tracked_parts,
+ },
+ {
+ async: false,
+ success: function(results) {
+ bom_items = results;
+ }
+ }
+ );
+ }
+
var table = options.table;
if (options.table == null) {
@@ -1002,13 +1379,72 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
setupFilterList('builditems', $(table), options.filterTarget);
- // If an "output" is specified, then only "trackable" parts are allocated
- // Otherwise, only "untrackable" parts are allowed
- var trackable = ! !output;
+ var allocated_items = output == null ? null : output.allocations;
- function reloadTable() {
- // Reload the entire build allocation table
- $(table).bootstrapTable('refresh');
+ function redrawAllocationData() {
+ // Force a refresh of each row in the table
+ // Note we cannot call 'refresh' because we are passing data from memory
+ // var rows = $(table).bootstrapTable('getData');
+
+ // How many rows are fully allocated?
+ var allocated_rows = 0;
+
+ bom_items.forEach(function(row) {
+ $(table).bootstrapTable('updateByUniqueId', row.pk, row, true);
+
+ if (isRowFullyAllocated(row)) {
+ allocated_rows += 1;
+ }
+ });
+
+ // Find the top-level progess bar for this build output
+ var output_progress_bar = $(`#output-progress-${outputId}`);
+
+ if (output_progress_bar.exists()) {
+ if (bom_items.length > 0) {
+ output_progress_bar.html(
+ makeProgressBar(
+ allocated_rows,
+ bom_items.length,
+ {
+ max_width: '150px',
+ }
+ )
+ );
+ }
+ } else {
+ console.warn(`Could not find progress bar for output '${outputId}'`);
+ }
+ }
+
+ function reloadAllocationData(async=true) {
+ // Reload stock allocation data for this particular build output
+
+ inventreeGet(
+ '{% url "api-build-item-list" %}',
+ {
+ build: buildId,
+ part_detail: true,
+ location_detail: true,
+ output: output == null ? null : output.pk,
+ },
+ {
+ async: async,
+ success: function(response) {
+ allocated_items = response;
+
+ redrawAllocationData();
+
+ }
+ }
+ );
+ }
+
+ if (allocated_items == null) {
+ // No allocation data provided? Request from server (blocking)
+ reloadAllocationData(false);
+ } else {
+ redrawAllocationData();
}
function requiredQuantity(row) {
@@ -1032,6 +1468,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
}
function availableQuantity(row) {
+ // Return the total available stock for a given row
// Base stock
var available = row.available_stock;
@@ -1045,27 +1482,17 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
}
return available;
-
}
- function sumAllocations(row) {
- // Calculat total allocations for a given row
- if (!row.allocations) {
- row.allocated = 0;
- return 0;
- }
-
- var quantity = 0;
-
- row.allocations.forEach(function(item) {
- quantity += item.quantity;
- });
-
- row.allocated = parseFloat(quantity.toFixed(15));
-
+ function allocatedQuantity(row) {
+ row.allocated = sumAllocationsForBomRow(row, allocated_items);
return row.allocated;
}
+ function isRowFullyAllocated(row) {
+ return allocatedQuantity(row) >= requiredQuantity(row);
+ }
+
function setupCallbacks() {
// Register button callbacks once table data are loaded
@@ -1079,7 +1506,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
if (!row) {
- console.log('WARNING: getRowByUniqueId returned null');
+ console.warn('getRowByUniqueId returned null');
return;
}
@@ -1092,7 +1519,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
{
source_location: buildInfo.source_location,
success: function(data) {
- $(table).bootstrapTable('refresh');
+ // $(table).bootstrapTable('refresh');
+ reloadAllocationData();
},
output: output == null ? null : output.pk,
}
@@ -1124,7 +1552,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
newBuildOrder({
part: pk,
parent: buildId,
- quantity: requiredQuantity(row) - sumAllocations(row),
+ quantity: requiredQuantity(row) - allocatedQuantity(row),
});
});
@@ -1139,18 +1567,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
bom_item: row.pk,
output: outputId == 'untracked' ? null : outputId,
table: table,
+ onSuccess: function(response, opts) {
+ reloadAllocationData();
+ }
});
});
}
// Load table of BOM items
$(table).inventreeTable({
- url: '{% url "api-bom-list" %}',
- queryParams: {
- part: partId,
- sub_part_detail: true,
- sub_part_trackable: trackable,
- },
+ data: bom_items,
disablePagination: true,
formatNoMatches: function() {
return '{% trans "No BOM items found" %}';
@@ -1162,124 +1588,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
// Setup button callbacks
setupCallbacks();
},
- onLoadSuccess: function(tableData) {
- // Once the BOM data are loaded, request allocation data for this build output
-
- var params = {
- build: buildId,
- part_detail: true,
- location_detail: true,
- };
-
- if (output) {
- params.sub_part_trackable = true;
- params.output = outputId;
- } else {
- params.sub_part_trackable = false;
- }
-
- inventreeGet('/api/build/item/',
- params,
- {
- success: function(data) {
- // Iterate through the returned data, and group by the part they point to
- var allocations = {};
-
- // Total number of line items
- var totalLines = tableData.length;
-
- // Total number of "completely allocated" lines
- var allocatedLines = 0;
-
- data.forEach(function(item) {
-
- // Group BuildItem objects by part
- var part = item.bom_part || item.part;
- var key = parseInt(part);
-
- if (!(key in allocations)) {
- allocations[key] = [];
- }
-
- allocations[key].push(item);
- });
-
- // Now update the allocations for each row in the table
- for (var key in allocations) {
-
- // Select the associated row in the table
- var tableRow = $(table).bootstrapTable('getRowByUniqueId', key);
-
- if (!tableRow) {
- continue;
- }
-
- // Set the allocation list for that row
- tableRow.allocations = allocations[key];
-
- // 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 >= requiredQuantity) {
- allocatedLines += 1;
- }
-
- // Push the updated row back into the main table
- $(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
- }
-
- // Update any rows which we did not receive allocation information for
- var td = $(table).bootstrapTable('getData');
-
- td.forEach(function(tableRow) {
- if (tableRow.allocations == null) {
-
- tableRow.allocations = [];
-
- $(table).bootstrapTable('updateByUniqueId', tableRow.pk, tableRow, true);
- }
- });
-
- // Update the progress bar for this build output
- var build_progress = $(`#output-progress-${outputId}`);
-
- if (build_progress.exists()) {
- if (totalLines > 0) {
-
- var progress = makeProgressBar(
- allocatedLines,
- totalLines,
- {
- max_width: '150px',
- }
- );
-
- build_progress.html(progress);
- } else {
- build_progress.html('');
- }
-
- } else {
- console.log(`WARNING: Could not find progress bar for output ${outputId}`);
- }
- }
- }
- );
- },
sortable: true,
showColumns: false,
detailView: true,
detailFilter: function(index, row) {
- return row.allocations != null;
+ return allocatedQuantity(row) > 0;
},
detailFormatter: function(index, row, element) {
// Contruct an 'inner table' which shows which stock items have been allocated
@@ -1293,7 +1606,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var subTable = $(`#${subTableId}`);
subTable.bootstrapTable({
- data: row.allocations,
+ data: getAllocationsForBomRow(row, allocated_items),
showHeader: true,
columns: [
{
@@ -1315,7 +1628,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var url = '';
-
var serial = row.serial;
if (row.stock_item_detail) {
@@ -1383,7 +1695,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
quantity: {},
},
title: '{% trans "Edit Allocation" %}',
- onSuccess: reloadTable,
+ onSuccess: reloadAllocationData,
});
});
@@ -1393,7 +1705,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
constructForm(`/api/build/item/${pk}/`, {
method: 'DELETE',
title: '{% trans "Remove Allocation" %}',
- onSuccess: reloadTable,
+ onSuccess: reloadAllocationData,
});
});
},
@@ -1494,25 +1806,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
title: '{% trans "Allocated" %}',
sortable: true,
formatter: function(value, row) {
- var allocated = 0;
-
- if (row.allocations != null) {
- row.allocations.forEach(function(item) {
- allocated += item.quantity;
- });
-
- var required = requiredQuantity(row);
-
- return makeProgressBar(allocated, required);
- } else {
- return `
{% trans "loading" %}...`;
- }
+ var allocated = allocatedQuantity(row);
+ var required = requiredQuantity(row);
+ return makeProgressBar(allocated, required);
},
sorter: function(valA, valB, rowA, rowB) {
// Custom sorting function for progress bars
- var aA = sumAllocations(rowA);
- var aB = sumAllocations(rowB);
+ var aA = allocatedQuantity(rowA);
+ var aB = allocatedQuantity(rowB);
var qA = requiredQuantity(rowA);
var qB = requiredQuantity(rowB);
@@ -1532,12 +1834,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
// Handle the case where both ratios are equal
if (progressA == progressB) {
- return (qA < qB) ? 1 : -1;
+ return (qA > qB) ? 1 : -1;
}
if (progressA == progressB) return 0;
- return (progressA < progressB) ? 1 : -1;
+ return (progressA > progressB) ? 1 : -1;
}
},
{
@@ -1547,7 +1849,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
// Generate action buttons for this build output
var html = `
`;
- if (sumAllocations(row) < requiredQuantity(row)) {
+ if (allocatedQuantity(row) < requiredQuantity(row)) {
if (row.sub_part_detail.assembly) {
html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}');
}
@@ -1563,7 +1865,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
'fa-minus-circle icon-red', 'button-unallocate', row.sub_part,
'{% trans "Unallocate stock" %}',
{
- disabled: row.allocations == null
+ disabled: allocatedQuantity(row) == 0,
}
);
@@ -1672,7 +1974,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
// var stock_input = constructRelatedFieldInput(`items_stock_item_${pk}`);
var html = `
-
+
${thumb} ${sub_part.full_name}
|
@@ -1762,8 +2064,6 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
method: 'POST',
fields: {},
preFormContent: html,
- confirm: true,
- confirmMessage: '{% trans "Confirm stock allocation" %}',
title: '{% trans "Allocate Stock Items to Build Order" %}',
afterRender: function(fields, options) {
@@ -1859,7 +2159,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
$(options.modal).find('.button-row-remove').click(function() {
var pk = $(this).attr('pk');
- $(options.modal).find(`#allocation_row_${pk}`).remove();
+ $(options.modal).find(`#items_${pk}`).remove();
});
},
onSubmit: function(fields, opts) {
@@ -1974,7 +2274,9 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
confirm: true,
preFormContent: html,
onSuccess: function(response) {
- $('#allocation-table-untracked').bootstrapTable('refresh');
+ if (options.onSuccess) {
+ options.onSuccess(response);
+ }
}
});
}
@@ -2072,8 +2374,8 @@ function loadBuildTable(table, options) {
}
},
{
- field: 'quantity',
- title: '{% trans "Completed" %}',
+ field: 'completed',
+ title: '{% trans "Progress" %}',
sortable: true,
formatter: function(value, row) {
return makeProgressBar(
diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js
index c464ad3645..dc40d1e30c 100644
--- a/InvenTree/templates/js/translated/helpers.js
+++ b/InvenTree/templates/js/translated/helpers.js
@@ -163,27 +163,29 @@ function makeProgressBar(value, maximum, opts={}) {
var style = options.style || '';
- var text = '';
+ var text = options.text;
+
+ if (!text) {
+ if (style == 'percent') {
+ // Display e.g. "50%"
- if (style == 'percent') {
- // Display e.g. "50%"
+ text = `${percent}%`;
+ } else if (style == 'max') {
+ // Display just the maximum value
+ text = `${maximum}`;
+ } else if (style == 'value') {
+ // Display just the current value
+ text = `${value}`;
+ } else if (style == 'blank') {
+ // No display!
+ text = '';
+ } else {
+ /* Default style
+ * Display e.g. "5 / 10"
+ */
- text = `${percent}%`;
- } else if (style == 'max') {
- // Display just the maximum value
- text = `${maximum}`;
- } else if (style == 'value') {
- // Display just the current value
- text = `${value}`;
- } else if (style == 'blank') {
- // No display!
- text = '';
- } else {
- /* Default style
- * Display e.g. "5 / 10"
- */
-
- text = `${value} / ${maximum}`;
+ text = `${value} / ${maximum}`;
+ }
}
var id = options.id || 'progress-bar';
diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js
index 656a3f9f63..b88de5af35 100644
--- a/InvenTree/templates/js/translated/model_renderers.js
+++ b/InvenTree/templates/js/translated/model_renderers.js
@@ -113,8 +113,6 @@ function renderStockItem(name, data, parameters={}, options={}) {
}
}
-
-
var html = `
${part_detail}
@@ -146,7 +144,7 @@ function renderStockLocation(name, data, parameters={}, options={}) {
html += ` - ${data.description}`;
}
- html += `{% trans "Location ID" %}: ${data.pk}`;
+ html += renderId('{% trans "Location ID" %}', data.pk, parameters);
return html;
}
@@ -162,10 +160,9 @@ function renderBuild(name, data, parameters={}, options={}) {
var html = select2Thumbnail(image);
- html += `${data.reference} - ${data.quantity} x ${data.part_detail.full_name}`;
- html += `{% trans "Build ID" %}: ${data.pk}`;
+ html += `${data.reference} - ${data.quantity} x ${data.part_detail.full_name}`;
- html += `${data.title}
`;
+ html += renderId('{% trans "Build ID" %}', data.pk, parameters);
return html;
}
@@ -300,12 +297,9 @@ function renderSalesOrderShipment(name, data, parameters={}, options={}) {
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
- var html = `
- ${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}
-
- {% trans "Shipment ID" %}: ${data.pk}
-
- `;
+ var html = `${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}`;
+
+ html += renderId('{% trans "Shipment ID" %}', data.pk, parameters);
return html;
}
@@ -323,7 +317,7 @@ function renderPartCategory(name, data, parameters={}, options={}) {
html += ` - ${data.description}`;
}
- html += `{% trans "Category ID" %}: ${data.pk}`;
+ html += renderId('{% trans "Category ID" %}', data.pk, parameters);
return html;
}
@@ -366,7 +360,7 @@ function renderManufacturerPart(name, data, parameters={}, options={}) {
html += ` ${data.manufacturer_detail.name} - ${data.MPN}`;
html += ` - ${data.part_detail.full_name}`;
- html += `{% trans "Manufacturer Part ID" %}: ${data.pk}`;
+ html += renderId('{% trans "Manufacturer Part ID" %}', data.pk, parameters);
return html;
}
@@ -395,9 +389,7 @@ function renderSupplierPart(name, data, parameters={}, options={}) {
html += ` ${data.supplier_detail.name} - ${data.SKU}`;
html += ` - ${data.part_detail.full_name}`;
- html += `{% trans "Supplier Part ID" %}: ${data.pk}`;
-
+ html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);
return html;
-
}