From ba3bcdba8916d93fac0c6e9b6d7d41caa67bf888 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 15:50:10 +1000 Subject: [PATCH 01/32] Add switchable columns to build output table --- InvenTree/templates/js/translated/build.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 65fc3a4d6c..8be3ee52ce 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -786,7 +786,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}`); } }); @@ -869,7 +869,7 @@ function loadBuildOutputTable(build_info, options={}) { url: '{% url "api-stock-list" %}', queryParams: filters, original: params, - showColumns: false, + showColumns: true, uniqueId: 'pk', name: 'build-outputs', sortable: true, @@ -901,6 +901,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 +910,9 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'quantity', - title: '{% trans "Quantity" %}', + title: '{% trans "Build Output" %}', + switchable: true, + sortable: true, formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; @@ -1079,7 +1082,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; } @@ -1269,7 +1272,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } } else { - console.log(`WARNING: Could not find progress bar for output ${outputId}`); + console.warn(`Could not find progress bar for output ${outputId}`); } } } From 63145c90b0c5fcbf0911065ea32209da980f7b09 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 16:14:23 +1000 Subject: [PATCH 02/32] Server-side sorting of "build output" column --- InvenTree/templates/js/translated/build.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 8be3ee52ce..115e55c902 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -913,6 +913,7 @@ function loadBuildOutputTable(build_info, options={}) { title: '{% trans "Build Output" %}', switchable: true, sortable: true, + sortName: 'stock', // This will sort by quantity -> serial_int -> serial formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; @@ -926,7 +927,7 @@ function loadBuildOutputTable(build_info, options={}) { } return renderLink(text, url); - } + }, }, { field: 'allocated', From 2b46ed300e2e13784a6de6d96907c6ea91822517 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 16:18:40 +1000 Subject: [PATCH 03/32] Client side pagination and sorting --- InvenTree/templates/js/translated/build.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 115e55c902..5d0461861e 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -874,7 +874,7 @@ function loadBuildOutputTable(build_info, options={}) { name: 'build-outputs', sortable: true, search: false, - sidePagination: 'server', + sidePagination: 'client', detailView: has_tracked_items, detailFilter: function(index, row) { return true; @@ -913,7 +913,6 @@ function loadBuildOutputTable(build_info, options={}) { title: '{% trans "Build Output" %}', switchable: true, sortable: true, - sortName: 'stock', // This will sort by quantity -> serial_int -> serial formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; @@ -928,6 +927,21 @@ function loadBuildOutputTable(build_info, options={}) { 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', From 6538ab86cb11c0e249b8861861bf4cb042ab3bed Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 16:25:04 +1000 Subject: [PATCH 04/32] Bug fix for 'required' filter in PartTestTemplate API --- InvenTree/part/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d894302e6258779c378d12d0cd24363ac2361a8b Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 16:54:02 +1000 Subject: [PATCH 05/32] Request build output test result information --- InvenTree/templates/js/translated/build.js | 131 ++++++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 5d0461861e..6a4e7ac429 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -865,6 +865,87 @@ function loadBuildOutputTable(build_info, options={}) { ); } + var part_tests = null; + + function updateTestResultData(rows) { + + console.log("updateTestResultData"); + + // 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, + }, + { + async: false, + success: function(response) { + // Save the list of part tests + part_tests = response; + + updateTestResultData(rows); + } + } + );; + } + + rows.forEach(function(row) { + + // Ignore if this row has already been updated (else, infinite loop!) + if (row.passed_tests) { + return; + } + + // Request test result information for the particular build output + inventreeGet( + '{% url "api-stock-test-result-list" %}', + { + stock_item: row.pk, + }, + { + success: function(results) { + + // A list of tests that this stock item has passed + var passed_tests = {}; + + // Keep a list of tests that this stock item has passed + results.forEach(function(result) { + if (result.result) { + passed_tests[result.key] = true; + } + }); + + row.passed_tests = passed_tests; + + $(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; + } + + var table_loaded = false; + $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -885,11 +966,21 @@ function loadBuildOutputTable(build_info, options={}) { formatNoMatches: function() { return '{% trans "No active build outputs found" %}'; }, - onPostBody: function() { + onPostBody: function(rows) { + console.log("onPostBody"); // Add callbacks for the buttons setupBuildOutputButtonCallbacks(); + }, + onLoadSuccess: function(rows) { - $(table).bootstrapTable('expandAllRows'); + console.log("onLoadSuccess"); + + updateTestResultData(rows); + }, + onRefresh: function() { + console.log("onRefresh"); + // var rows = $(table).bootstrapTable('getData'); + // updateTestResultData(rows); }, columns: [ { @@ -945,10 +1036,44 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'allocated', - title: '{% trans "Allocated Parts" %}', + title: '{% trans "Allocated Stock" %}', visible: has_tracked_items, + switchable: false, formatter: function(value, row) { return `
`; + }, + sorter: function(value_a, value_b, row_a, row_b) { + // TODO: Custom sorter for "allocated stock" column + return 0; + }, + }, + { + field: 'tests', + title: '{% trans "Tests" %}', + sortable: false, + switchable: true, + formatter: function(value, row) { + if (part_tests == null || part_tests.length == 0) { + return `{% trans "No tests found " %}`; + } + + 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; } }, { From a48c8025760cb38c2a444ebeea1cf398adcd38b1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 16:54:29 +1000 Subject: [PATCH 06/32] Sort by test status --- InvenTree/templates/js/translated/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 6a4e7ac429..111cb51b15 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1050,7 +1050,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'tests', title: '{% trans "Tests" %}', - sortable: false, + sortable: true, switchable: true, formatter: function(value, row) { if (part_tests == null || part_tests.length == 0) { From 74a08be5be0759e28629e478e0cf5401a358be7f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 17:00:23 +1000 Subject: [PATCH 07/32] Load (and cache) tracked BOM items for this build output --- InvenTree/templates/js/translated/build.js | 46 +++++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 111cb51b15..f7399a50df 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -865,9 +865,44 @@ function loadBuildOutputTable(build_info, options={}) { ); } + // List of "tracked bom items" required for this build order + var bom_items = null; + + function updateAllocationData(rows) { + // Update stock allocation information for the build outputs + + console.log("updateAllocationData"); + + // Request list of BOM data for this build order + if (bom_items == null) { + inventreeGet( + '{% url "api-bom-list" %}', + { + part: build_info.part, + sub_part_detail: true, + sub_part_trackable: true, + }, + { + success: function(response) { + // Save the BOM items + bom_items = response; + + // Callback to this function again + updateAllocationData(rows); + } + } + ); + + return; + } + + console.log("BOM ITEMS:", bom_items); + } + var part_tests = null; function updateTestResultData(rows) { + // Update test result information for the build outputs console.log("updateTestResultData"); @@ -880,15 +915,17 @@ function loadBuildOutputTable(build_info, options={}) { required: true, }, { - async: false, success: function(response) { // Save the list of part tests part_tests = response; + // Callback to this function again updateTestResultData(rows); } } - );; + ); + + return; } rows.forEach(function(row) { @@ -944,8 +981,6 @@ function loadBuildOutputTable(build_info, options={}) { return n; } - var table_loaded = false; - $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -975,12 +1010,11 @@ function loadBuildOutputTable(build_info, options={}) { console.log("onLoadSuccess"); + updateAllocationData(rows); updateTestResultData(rows); }, onRefresh: function() { console.log("onRefresh"); - // var rows = $(table).bootstrapTable('getData'); - // updateTestResultData(rows); }, columns: [ { From e6c95bf6b21f219a5e6b7d96ba493fa95a385570 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 17:08:43 +1000 Subject: [PATCH 08/32] Cache tracked BOM items for the build order --- InvenTree/templates/js/translated/build.js | 50 +++++++++++++--------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index f7399a50df..f8872f12a5 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -841,6 +841,9 @@ function loadBuildOutputTable(build_info, options={}) { }); } + // List of "tracked bom items" required for this build order + var bom_items = null; + /* * Construct a "sub table" showing the required BOM items */ @@ -855,6 +858,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,14 +871,9 @@ function loadBuildOutputTable(build_info, options={}) { ); } - // List of "tracked bom items" required for this build order - var bom_items = null; - function updateAllocationData(rows) { // Update stock allocation information for the build outputs - console.log("updateAllocationData"); - // Request list of BOM data for this build order if (bom_items == null) { inventreeGet( @@ -896,7 +897,9 @@ function loadBuildOutputTable(build_info, options={}) { return; } - console.log("BOM ITEMS:", bom_items); + rows.forEach(function(row) { + + }) } var part_tests = null; @@ -904,8 +907,6 @@ function loadBuildOutputTable(build_info, options={}) { function updateTestResultData(rows) { // Update test result information for the build outputs - console.log("updateTestResultData"); - // Request test template data if it has not already been retrieved if (part_tests == null) { inventreeGet( @@ -1002,20 +1003,14 @@ function loadBuildOutputTable(build_info, options={}) { return '{% trans "No active build outputs found" %}'; }, onPostBody: function(rows) { - console.log("onPostBody"); // Add callbacks for the buttons setupBuildOutputButtonCallbacks(); }, onLoadSuccess: function(rows) { - console.log("onLoadSuccess"); - updateAllocationData(rows); updateTestResultData(rows); }, - onRefresh: function() { - console.log("onRefresh"); - }, columns: [ { title: '', @@ -1320,14 +1315,29 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }); } + 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: trackable, + }, + { + async: false, + success: function(results) { + bom_items = results; + } + } + ); + } + // 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" %}'; From 0841c628e0ba8626aad14882e76753ede63b0c8c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 17:27:09 +1000 Subject: [PATCH 09/32] Adds ability to filter build items by "tracked" flag --- InvenTree/build/api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 33f3f4ab36..2e2fb53510 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -442,6 +442,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 = queryest.filter(install_into=None) + # Filter by output target output = params.get('output', None) From b8f274c680131247cf4a3ff9e59f2421f1c50bec Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 17:38:08 +1000 Subject: [PATCH 10/32] Request allocations for entire build, and cache --- InvenTree/templates/js/translated/build.js | 73 ++++++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index f8872f12a5..aa35d4c011 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -897,9 +897,46 @@ function loadBuildOutputTable(build_info, options={}) { return; } - rows.forEach(function(row) { + // 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); + + console.log("Updating row for stock item", row.pk); + }); + } + } + ) } var part_tests = null; @@ -1145,7 +1182,6 @@ function loadBuildOutputTable(build_info, options={}) { */ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { - var buildId = buildInfo.pk; var partId = buildInfo.part; @@ -1178,6 +1214,30 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Otherwise, only "untrackable" parts are allowed var trackable = ! !output; + var allocated_items = output == null ? null : output.allocations; + + if (allocated_items == null) { + + inventreeGet( + '{% url "api-build-item-list" %}', + { + build: buildId, + part_detail: true, + location_detail: true, + output: output == null ? null : output.pk, + }, + { + async: false, + success: function(response) { + allocated_items = response; + } + } + ); + } + + console.log("rendering table for output:", outputId); + console.log("allocations:", allocated_items); + function reloadTable() { // Reload the entire build allocation table $(table).bootstrapTable('refresh'); @@ -1348,10 +1408,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { onPostBody: function(data) { // Setup button callbacks setupCallbacks(); - }, - onLoadSuccess: function(tableData) { + // }, + // onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output + console.log("old onLoadSuccess"); + return; + var params = { build: buildId, part_detail: true, From 72bcea2f6dac73851a5c4da95c2d6aab01b60e38 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 28 Apr 2022 17:53:27 +1000 Subject: [PATCH 11/32] Better caching and rendering of sub tables for particular build outputs --- InvenTree/templates/js/translated/build.js | 172 ++++----------------- 1 file changed, 28 insertions(+), 144 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index aa35d4c011..820f78f47b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -931,8 +931,6 @@ function loadBuildOutputTable(build_info, options={}) { rows.forEach(function(row) { row.allocations = allocations[row.pk] || []; $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); - - console.log("Updating row for stock item", row.pk); }); } } @@ -1280,24 +1278,37 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } - function sumAllocations(row) { - // Calculat total allocations for a given row - if (!row.allocations) { - row.allocated = 0; - return 0; - } + function getAllocationsForRow(row) { + var part_id = row.sub_part; - var quantity = 0; + var allocations = []; - row.allocations.forEach(function(item) { - quantity += item.quantity; + allocated_items.forEach(function(allocation) { + if (allocation.bom_part == part_id) { + allocations.push(allocation); + } }); - row.allocated = parseFloat(quantity.toFixed(15)); + return allocations; + } + + function sumAllocations(row) { + + var allocated_quantity = 0; + + getAllocationsForRow(row).forEach(function(allocation) { + allocated_quantity += allocation.quantity; + }); + + row.allocated = parseFloat(allocated_quantity.toFixed(15)); return row.allocated; } + function isRowFullyAllocated(row) { + return sumAllocations(row) >= requiredQuantity(row); + } + function setupCallbacks() { // Register button callbacks once table data are loaded @@ -1408,128 +1419,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { onPostBody: function(data) { // Setup button callbacks setupCallbacks(); - // }, - // onLoadSuccess: function(tableData) { - // Once the BOM data are loaded, request allocation data for this build output - - console.log("old onLoadSuccess"); - return; - - 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.warn(`Could not find progress bar for output ${outputId}`); - } - } - } - ); }, sortable: true, showColumns: false, detailView: true, detailFilter: function(index, row) { - return row.allocations != null; + return sumAllocations(row) > 0; }, detailFormatter: function(index, row, element) { // Contruct an 'inner table' which shows which stock items have been allocated @@ -1543,7 +1438,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var subTable = $(`#${subTableId}`); subTable.bootstrapTable({ - data: row.allocations, + data: getAllocationsForRow(row), showHeader: true, columns: [ { @@ -1565,7 +1460,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var url = ''; - var serial = row.serial; if (row.stock_item_detail) { @@ -1744,19 +1638,9 @@ 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 = sumAllocations(row); + var required = requiredQuantity(row) + return makeProgressBar(allocated, required); }, sorter: function(valA, valB, rowA, rowB) { // Custom sorting function for progress bars From f531e354b95ace9e0673a9b3c27756497cdf7c80 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 28 Apr 2022 21:54:38 +1000 Subject: [PATCH 12/32] Allow completion of partially allocated build outputs - Requires manual acceptance from user --- InvenTree/build/api.py | 2 +- InvenTree/build/serializers.py | 15 ++++++++++++++- InvenTree/templates/js/translated/build.js | 6 ++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 2e2fb53510..22dd6473ab 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -452,7 +452,7 @@ class BuildItemList(generics.ListCreateAPIView): if tracked: queryset = queryset.exclude(install_into=None) else: - queryset = queryest.filter(install_into=None) + queryset = queryset.filter(install_into=None) # Filter by output target output = params.get('output', None) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 4b55182563..7df2c474a9 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, diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 820f78f47b..29683154fe 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -427,6 +427,8 @@ function completeBuildOutputs(build_id, outputs, options={}) { fields: { status: {}, location: {}, + notes: {}, + accept_incomplete_allocation: {}, }, confirm: true, title: '{% trans "Complete Build Outputs" %}', @@ -445,6 +447,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 = []; @@ -1896,8 +1900,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) { From b63352ce200ce60f15a773bbd73644f296c7c16c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 28 Apr 2022 22:58:58 +1000 Subject: [PATCH 13/32] Add a stock item transaction when installing items into a build output --- InvenTree/build/models.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index e5189e6073..61783fc8fb 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( From cb7c4396fb160043a0946ee1bea15721f875363b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 28 Apr 2022 22:59:24 +1000 Subject: [PATCH 14/32] Refactor build page template - Only load build outputs table as required --- InvenTree/build/templates/build/detail.html | 177 ++++++++++---------- InvenTree/templates/js/translated/build.js | 16 +- 2 files changed, 89 insertions(+), 104 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 92e1177e0f..c01c9055f1 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -401,110 +401,107 @@ 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', - ] - ); + linkButtonsToSelection( + '#build-output-table', + [ + '#output-options', + '#multi-output-complete', + '#multi-output-delete', + ] + ); - $('#multi-output-complete').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); + $('#multi-output-complete').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); - completeBuildOutputs( - build_info.pk, - outputs, - { - success: function() { - // Reload the "in progress" table - $('#build-output-table').bootstrapTable('refresh'); + 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'); - } - } - ); - }); - - $('#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'); + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); } + } + ); + }); - var stock_id_values = []; + $('#multi-output-delete').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); - outputs.forEach(function(output) { - stock_id_values.push(output.pk); - }); + deleteBuildOutputs( + build_info.pk, + outputs, + { + success: function() { + // Reload the "in progress" table + $('#build-output-table').bootstrapTable('refresh'); - 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, + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); } - ); - {% endif %} + } + ) + }); + + $('#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 %} + +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, +}; + +// Load allocation table for un-tracked parts +loadBuildOutputAllocationTable( + build_info, + null, + { + search: true, } ); +{% endif %} $('#btn-create-output').click(function() { diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 29683154fe..b90837a231 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -741,18 +741,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) { @@ -1031,7 +1019,7 @@ function loadBuildOutputTable(build_info, options={}) { sortable: true, search: false, sidePagination: 'client', - detailView: has_tracked_items, + detailView: true, detailFilter: function(index, row) { return true; }, @@ -1105,7 +1093,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'allocated', title: '{% trans "Allocated Stock" %}', - visible: has_tracked_items, + visible: true, switchable: false, formatter: function(value, row) { return `
`; From 4a81c058d60c46978ace380ae90d16fd3fc6a0df Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 28 Apr 2022 23:14:37 +1000 Subject: [PATCH 15/32] Function to reload allocation table data --- InvenTree/templates/js/translated/build.js | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index b90837a231..7705bfaa16 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -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'); } @@ -1206,7 +1208,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var allocated_items = output == null ? null : output.allocations; - if (allocated_items == null) { + function reloadAllocationData(async=true) { + // Reload stock allocation data for this particular build output inventreeGet( '{% url "api-build-item-list" %}', @@ -1217,16 +1220,37 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { output: output == null ? null : output.pk, }, { - async: false, + async: async, success: function(response) { allocated_items = response; + + if (async) { + + // 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; + + rows.forEach(function(row) { + $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + + if (isRowFullyAllocated(row)) { + allocated_rows += 1; + } + }); + } } } ); } - console.log("rendering table for output:", outputId); - console.log("allocations:", allocated_items); + if (allocated_items == null) { + + // No allocation data provided? Request from server (blocking) + reloadAllocationData(false); + } function reloadTable() { // Reload the entire build allocation table @@ -1254,6 +1278,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; @@ -1327,7 +1352,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, } @@ -1374,6 +1400,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { bom_item: row.pk, output: outputId == 'untracked' ? null : outputId, table: table, + onSuccess: function(response, opts) { + reloadAllocationData(); + } }); }); } From df9a33935def45d7a21406209e60713363a3d75f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 28 Apr 2022 23:26:08 +1000 Subject: [PATCH 16/32] Row button fixes --- InvenTree/templates/js/translated/build.js | 53 +++++++++++----------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 7705bfaa16..3376cb21a0 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -838,6 +838,23 @@ 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 */ @@ -868,29 +885,6 @@ function loadBuildOutputTable(build_info, options={}) { function updateAllocationData(rows) { // Update stock allocation information for the build outputs - // Request list of BOM data for this build order - if (bom_items == null) { - inventreeGet( - '{% url "api-bom-list" %}', - { - part: build_info.part, - sub_part_detail: true, - sub_part_trackable: true, - }, - { - success: function(response) { - // Save the BOM items - bom_items = response; - - // Callback to this function again - updateAllocationData(rows); - } - } - ); - - return; - } - // Request updated stock allocation data for this build order inventreeGet( '{% url "api-build-item-list" %}', @@ -1098,6 +1092,13 @@ function loadBuildOutputTable(build_info, options={}) { visible: true, switchable: false, formatter: function(value, row) { + // Display a progress bar which shows how many rows have been allocated + var n_bom_lines = 0; + + if (bom_items) { + n_bom_lines = bom_items.length; + } + return `lines: ${n_bom_lines}`; return `
`; }, sorter: function(value_a, value_b, row_a, row_b) { @@ -1548,7 +1549,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { quantity: {}, }, title: '{% trans "Edit Allocation" %}', - onSuccess: reloadTable, + onSuccess: reloadAllocationData, }); }); @@ -1558,7 +1559,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { constructForm(`/api/build/item/${pk}/`, { method: 'DELETE', title: '{% trans "Remove Allocation" %}', - onSuccess: reloadTable, + onSuccess: reloadAllocationData, }); }); }, @@ -1718,7 +1719,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { 'fa-minus-circle icon-red', 'button-unallocate', row.sub_part, '{% trans "Unallocate stock" %}', { - disabled: row.allocations == null + disabled: sumAllocations(row) == 0, } ); From 6c6ebe70c27a940f22bcfaa89fbb94fac60846f4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 00:27:27 +1000 Subject: [PATCH 17/32] Update progress bars for build output allocation --- InvenTree/templates/js/translated/build.js | 227 ++++++++++++------- InvenTree/templates/js/translated/helpers.js | 40 ++-- 2 files changed, 171 insertions(+), 96 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 3376cb21a0..2c0b56c518 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -726,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. * @@ -919,6 +948,32 @@ function loadBuildOutputTable(build_info, options={}) { 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', + } + ) + ); + } + }); }); } } @@ -1092,14 +1147,33 @@ function loadBuildOutputTable(build_info, options={}) { visible: true, switchable: false, formatter: function(value, row) { - // Display a progress bar which shows how many rows have been allocated - var n_bom_lines = 0; + + // Display a progress bar which shows how many BOM lines have been fully allocated + var n_bom_lines = 1; + var n_completed_lines = 0; - if (bom_items) { + // Work out how many lines have been allocated for this build output + if (bom_items && row.allocations) { n_bom_lines = bom_items.length; + + bom_items.forEach(function(bom_row) { + var required_quantity = row.quantity * bom_row.quantity; + + if (sumAllocationsForBomRow(bom_row, row.allocations) >= required_quantity) { + n_completed_lines += 1; + } + }) } - return `lines: ${n_bom_lines}`; - return `
`; + + var progressBar = makeProgressBar( + n_completed_lines, + n_bom_lines, + { + max_width: '150px', + } + ); + + return `
${progressBar}
`; }, sorter: function(value_a, value_b, row_a, row_b) { // TODO: Custom sorter for "allocated stock" column @@ -1108,7 +1182,7 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'tests', - title: '{% trans "Tests" %}', + title: '{% trans "Completed Tests" %}', sortable: true, switchable: true, formatter: function(value, row) { @@ -1186,6 +1260,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: trackable, + }, + { + async: false, + success: function(results) { + bom_items = results; + } + } + ); + } + var table = options.table; if (options.table == null) { @@ -1209,6 +1303,42 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var allocated_items = output == null ? null : output.allocations; + 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 @@ -1225,32 +1355,18 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { success: function(response) { allocated_items = response; - if (async) { + 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; - - rows.forEach(function(row) { - $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); - - if (isRowFullyAllocated(row)) { - allocated_rows += 1; - } - }); - } } } ); } if (allocated_items == null) { - // No allocation data provided? Request from server (blocking) reloadAllocationData(false); + } else { + redrawAllocationData(); } function reloadTable() { @@ -1293,38 +1409,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } return available; - } - function getAllocationsForRow(row) { - var part_id = row.sub_part; - - var allocations = []; - - allocated_items.forEach(function(allocation) { - if (allocation.bom_part == part_id) { - allocations.push(allocation); - } - }); - - return allocations; - } - - function sumAllocations(row) { - - var allocated_quantity = 0; - - getAllocationsForRow(row).forEach(function(allocation) { - allocated_quantity += allocation.quantity; - }); - - row.allocated = parseFloat(allocated_quantity.toFixed(15)); - + function allocatedQuantity(row) { + row.allocated = sumAllocationsForBomRow(row, allocated_items); return row.allocated; } function isRowFullyAllocated(row) { - return sumAllocations(row) >= requiredQuantity(row); + return allocatedQuantity(row) >= requiredQuantity(row); } function setupCallbacks() { @@ -1386,7 +1479,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { newBuildOrder({ part: pk, parent: buildId, - quantity: requiredQuantity(row) - sumAllocations(row), + quantity: requiredQuantity(row) - allocatedQuantity(row), }); }); @@ -1408,26 +1501,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }); } - 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: trackable, - }, - { - async: false, - success: function(results) { - bom_items = results; - } - } - ); - } - // Load table of BOM items $(table).inventreeTable({ data: bom_items, @@ -1446,7 +1519,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { showColumns: false, detailView: true, detailFilter: function(index, row) { - return sumAllocations(row) > 0; + return allocatedQuantity(row) > 0; }, detailFormatter: function(index, row, element) { // Contruct an 'inner table' which shows which stock items have been allocated @@ -1460,7 +1533,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var subTable = $(`#${subTableId}`); subTable.bootstrapTable({ - data: getAllocationsForRow(row), + data: getAllocationsForBomRow(row, allocated_items), showHeader: true, columns: [ { @@ -1660,15 +1733,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { title: '{% trans "Allocated" %}', sortable: true, formatter: function(value, row) { - var allocated = sumAllocations(row); + 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); @@ -1703,7 +1776,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" %}'); } @@ -1719,7 +1792,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { 'fa-minus-circle icon-red', 'button-unallocate', row.sub_part, '{% trans "Unallocate stock" %}', { - disabled: sumAllocations(row) == 0, + disabled: allocatedQuantity(row) == 0, } ); 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'; From 3da644637380a83e605a20cc420a7ffcb0f3dfc6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 00:32:33 +1000 Subject: [PATCH 18/32] Allow sorting of build output table by allocated items --- InvenTree/templates/js/translated/build.js | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 2c0b56c518..c91dc1ccc0 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1060,6 +1060,21 @@ function loadBuildOutputTable(build_info, options={}) { 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, @@ -1146,28 +1161,12 @@ function loadBuildOutputTable(build_info, options={}) { title: '{% trans "Allocated Stock" %}', visible: true, switchable: false, + sortable: true, formatter: function(value, row) { - // Display a progress bar which shows how many BOM lines have been fully allocated - var n_bom_lines = 1; - var n_completed_lines = 0; - - // Work out how many lines have been allocated for this build output - if (bom_items && row.allocations) { - n_bom_lines = bom_items.length; - - bom_items.forEach(function(bom_row) { - var required_quantity = row.quantity * bom_row.quantity; - - if (sumAllocationsForBomRow(bom_row, row.allocations) >= required_quantity) { - n_completed_lines += 1; - } - }) - } - var progressBar = makeProgressBar( - n_completed_lines, - n_bom_lines, + countAllocatedLines(row), + bom_items.length, { max_width: '150px', } @@ -1176,8 +1175,10 @@ function loadBuildOutputTable(build_info, options={}) { return `
${progressBar}
`; }, sorter: function(value_a, value_b, row_a, row_b) { - // TODO: Custom sorter for "allocated stock" column - return 0; + var q_a = countAllocatedLines(row_a); + var q_b = countAllocatedLines(row_b); + + return q_a > q_b ? 1 : -1; }, }, { From 6e52ca21785c146ec6c1a0df83843b14de0b6090 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 00:44:29 +1000 Subject: [PATCH 19/32] Refactor button callbacks - Add button to expand all output rows - Add button to collapse all output rows --- InvenTree/build/templates/build/detail.html | 72 +++----------------- InvenTree/templates/js/translated/build.js | 73 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index c01c9055f1..52b81e6389 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 %} @@ -416,68 +426,6 @@ onPanelLoad('outputs', function() { loadBuildOutputTable(build_info); - linkButtonsToSelection( - '#build-output-table', - [ - '#output-options', - '#multi-output-complete', - '#multi-output-delete', - ] - ); - - $('#multi-output-complete').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); - - 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'); - } - } - ); - }); - - $('#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 %} }); diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index c91dc1ccc0..935206ce2f 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1233,6 +1233,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'); + }); } From 81638d06cf06901bf879c5d336ec4449b9e60443 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 00:51:56 +1000 Subject: [PATCH 20/32] Show or hide items based on output BOM --- InvenTree/templates/js/translated/build.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 935206ce2f..705060ce63 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( @@ -1085,9 +1085,9 @@ function loadBuildOutputTable(build_info, options={}) { sortable: true, search: false, sidePagination: 'client', - detailView: true, + 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); @@ -1159,11 +1159,15 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'allocated', title: '{% trans "Allocated Stock" %}', - visible: true, + visible: bom_items.length > 0, switchable: false, sortable: true, formatter: function(value, row) { + if (bom_items.length == 0) { + return `
{% trans "No tracked BOM items for this build" %}
`; + } + var progressBar = makeProgressBar( countAllocatedLines(row), bom_items.length, @@ -1188,7 +1192,7 @@ function loadBuildOutputTable(build_info, options={}) { switchable: true, formatter: function(value, row) { if (part_tests == null || part_tests.length == 0) { - return `{% trans "No tests found " %}`; + return `{% trans "No required tests for this build" %}`; } var n_passed = countPassedTests(row); @@ -1218,6 +1222,9 @@ function loadBuildOutputTable(build_info, options={}) { return makeBuildOutputButtons( row.pk, build_info, + { + has_bom_items: bom_items.length > 0, + } ); } } From 6b4592b3dcc2349f00a4125ff4cf9a1c6e13d628 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 01:10:05 +1000 Subject: [PATCH 21/32] Display error if stock item is "double allocted" --- InvenTree/build/serializers.py | 9 +++++++-- InvenTree/templates/js/translated/build.js | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 7df2c474a9..d037ad546e 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -630,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'] @@ -654,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/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 705060ce63..c996501060 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1982,7 +1982,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} @@ -2167,7 +2167,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) { From e0189be5a6ec0e92ba8ee67fa63d2a5f0cd1fbea Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 01:19:36 +1000 Subject: [PATCH 22/32] Adds ability to filter StockItemTestresult API list by Build ID - Allows us to retrieve stock item test results in a single API query --- InvenTree/stock/api.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index d4fc5c93d1..f6b21ca5af 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,20 @@ 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) From a0ca20ab04e71d978b2519dcd557af78962420fb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 01:27:58 +1000 Subject: [PATCH 23/32] Retrieve all stock item test results at once --- InvenTree/templates/js/translated/build.js | 45 ++++++++++------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index c996501060..5fe9165c9d 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1007,39 +1007,36 @@ function loadBuildOutputTable(build_info, options={}) { return; } - rows.forEach(function(row) { + // Retrieve stock results for the entire build + inventreeGet( + '{% url "api-stock-test-result-list" %}', + { + build: build_info.pk, + }, + { + success: function(results) { - // Ignore if this row has already been updated (else, infinite loop!) - if (row.passed_tests) { - return; - } + // Iterate through each row and find matching test results + rows.forEach(function(row) { + var test_results = {}; - // Request test result information for the particular build output - inventreeGet( - '{% url "api-stock-test-result-list" %}', - { - stock_item: row.pk, - }, - { - success: function(results) { - - // A list of tests that this stock item has passed - var passed_tests = {}; - - // Keep a list of tests that this stock item has passed results.forEach(function(result) { - if (result.result) { - passed_tests[result.key] = true; + 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 = passed_tests; + row.passed_tests = test_results; $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); - } + }); } - ) - }); + } + ); } // Return the number of 'passed' tests in a given row From 0bda9c974e1389a8ef08bdcb1fab9db1fab5e9fa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 01:35:09 +1000 Subject: [PATCH 24/32] PEP fixes --- InvenTree/build/models.py | 2 +- InvenTree/stock/api.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 61783fc8fb..86bb256539 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1292,7 +1292,7 @@ class BuildItem(models.Model): user, notes ) - + else: # Simply remove the items from stock item.take_stock( diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index f6b21ca5af..c88c29e64e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -1165,7 +1165,7 @@ class StockItemTestResultList(generics.ListCreateAPIView): build = params.get('build', None) if build is not None: - + try: build = Build.objects.get(pk=build) @@ -1174,7 +1174,6 @@ class StockItemTestResultList(generics.ListCreateAPIView): except (ValueError, Build.DoesNotExist): pass - # Filter by stock item item = params.get('stock_item', None) From b595fa0e7eb47d734d3d2c35cffb65e18e72d425 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 01:40:59 +1000 Subject: [PATCH 25/32] Fix loading of untracked parts table --- InvenTree/templates/js/translated/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 5fe9165c9d..502f95d65f 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1347,7 +1347,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { { part: partId, sub_part_detail: true, - sub_part_trackable: trackable, + sub_part_trackable: buildInfo.tracked_parts, }, { async: false, From 51da1f02a8d126e32fa84da7f1057ad52f13afb4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 29 Apr 2022 01:58:57 +1000 Subject: [PATCH 26/32] JS linting fixes --- InvenTree/templates/js/translated/build.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 502f95d65f..a88f3ccb28 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -867,7 +867,7 @@ 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 + // Request list of BOM data for this build order inventreeGet( '{% url "api-bom-list" %}', { @@ -962,7 +962,7 @@ function loadBuildOutputTable(build_info, options={}) { var output_progress_bar = $(`#output-progress-${row.pk}`); - if (output_progress_bar.exists()) { + if (output_progress_bar.exists()) { output_progress_bar.html( makeProgressBar( n_completed_lines, @@ -977,7 +977,7 @@ function loadBuildOutputTable(build_info, options={}) { }); } } - ) + ); } var part_tests = null; @@ -1283,14 +1283,14 @@ function loadBuildOutputTable(build_info, options={}) { $('#build-stock-table').bootstrapTable('refresh'); } } - ) + ); }); // Print stock item labels $('#incomplete-output-print-label').click(function() { var outputs = $(table).bootstrapTable('getSelections'); - if (outputs.length == 0) { + if (outputs.length == 0) { outputs = $(table).bootstrapTable('getData'); } @@ -1375,10 +1375,6 @@ 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 redrawAllocationData() { @@ -1447,11 +1443,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { redrawAllocationData(); } - function reloadTable() { - // Reload the entire build allocation table - $(table).bootstrapTable('refresh'); - } - function requiredQuantity(row) { // Return the requied quantity for a given row @@ -1812,7 +1803,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { sortable: true, formatter: function(value, row) { var allocated = allocatedQuantity(row); - var required = requiredQuantity(row) + var required = requiredQuantity(row); return makeProgressBar(allocated, required); }, sorter: function(valA, valB, rowA, rowB) { From 8fc34a21a6b7be239c00743ce3e14a91ecd52669 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 29 Apr 2022 12:59:20 +1000 Subject: [PATCH 27/32] Reload the untracked stock table when allocation actions are performed --- InvenTree/build/templates/build/detail.html | 52 ++++++++++++--------- InvenTree/templates/js/translated/build.js | 4 +- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 52b81e6389..42bc51bb2f 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -431,24 +431,32 @@ onPanelLoad('outputs', function() { {% if build.active and build.has_untracked_bom_items %} -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, -}; +function loadUntrackedStockTable() { + + 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'); + + // Load allocation table for un-tracked parts + loadBuildOutputAllocationTable( + build_info, + null, + { + search: true, + } + ); +} + +loadUntrackedStockTable(); -// Load allocation table for un-tracked parts -loadBuildOutputAllocationTable( - build_info, - null, - { - search: true, - } -); {% endif %} $('#btn-create-output').click(function() { @@ -472,6 +480,7 @@ $("#btn-auto-allocate").on('click', function() { {% if build.take_from %} location: {{ build.take_from.pk }}, {% endif %} + onSuccess: loadUntrackedStockTable, } ); }); @@ -503,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, } ); } @@ -514,6 +521,7 @@ $("#btn-allocate").on('click', function() { $('#btn-unallocate').on('click', function() { unallocateStock({{ build.id }}, { table: '#allocation-table-untracked', + onSuccess: loadUntrackedStockTable, }); }); @@ -533,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/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index a88f3ccb28..a8a973d516 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -2270,7 +2270,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); + } } }); } From a80465e85cbdab6093479c97c18a7bb04a547d0c Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 29 Apr 2022 13:03:41 +1000 Subject: [PATCH 28/32] Display batch code in build output table --- InvenTree/templates/js/translated/build.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index a8a973d516..719df6ba5c 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1135,6 +1135,10 @@ 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) { From 94fa424440b033fc306c6bbc1db64fb0606b7ff6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 29 Apr 2022 13:13:12 +1000 Subject: [PATCH 29/32] Table tweaks --- InvenTree/build/api.py | 1 + InvenTree/templates/js/translated/build.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 22dd6473ab..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', ] diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 719df6ba5c..c9ebbe0e22 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1121,7 +1121,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'quantity', title: '{% trans "Build Output" %}', - switchable: true, + switchable: false, sortable: true, formatter: function(value, row) { @@ -1834,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; } }, { @@ -2374,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( From 1bb2551edb2afff7543c86cdc0566bd9c5fb8521 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 29 Apr 2022 13:51:49 +1000 Subject: [PATCH 30/32] Fixes for model rendering code --- .../js/translated/model_renderers.js | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) 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; - } From 260680c5c494156dcb628618e461964f83e04580 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 29 Apr 2022 17:07:54 +1000 Subject: [PATCH 31/32] Refactor BOM table to not load multi level BOMs by default - Click to select which ones to load --- InvenTree/templates/js/translated/bom.js | 86 +++++++++++------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 2d7796edcd..c3180bf490 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); - } - } }, }); From 35de490f72b75b28ffdfee3a39295f145e87adf5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 29 Apr 2022 17:13:59 +1000 Subject: [PATCH 32/32] JS linting fixes --- InvenTree/templates/js/translated/bom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index c3180bf490..e4674d5989 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -1116,8 +1116,8 @@ function loadBomTable(table, options={}) { table.treegrid('collapseAll'); - // Callback for 'load sub assembly' button - $(table).find('.load-sub-assembly').click(function(event) { + // Callback for 'load sub assembly' button + $(table).find('.load-sub-assembly').click(function(event) { event.preventDefault();