2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 04:55:44 +00:00

Merge branch 'inventree:master' into matmair/issue2694

This commit is contained in:
Matthias Mair
2022-04-30 19:50:25 +02:00
committed by GitHub
24 changed files with 762 additions and 492 deletions

View File

@ -13,15 +13,15 @@
{% endblock %}
{% block actions %}
{% inventree_demo_mode as demo %}
{% if not demo %}
{% inventree_customize 'hide_password_reset' as hide_password_reset %}
{% if not hide_password_reset %}
<div class='btn btn-outline-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %}
</div>
{% endif %}
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div>
{% endif %}
{% endblock %}
{% block content %}

View File

@ -12,7 +12,6 @@
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
{% inventree_customize 'login_message' as login_message %}
{% mail_configured as mail_conf %}
{% inventree_demo_mode as demo %}
<h1>{% trans "Sign In" %}</h1>
@ -37,12 +36,12 @@ for a account and sign in below:{% endblocktrans %}</p>
<hr>
{% if login_message %}
<div>{{ login_message }}<hr></div>
<div>{{ login_message | safe }}<hr></div>
{% endif %}
<div class="btn-group float-right" role="group">
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
</div>
{% if mail_conf and enable_pwd_forgot and not demo %}
{% if mail_conf and enable_pwd_forgot %}
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
{% endif %}
</form>

View File

@ -8,7 +8,6 @@
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
{% settings_value "LABEL_ENABLE" with user=user as labels_enabled %}
{% inventree_show_about user as show_about %}
{% inventree_demo_mode as demo_mode %}
<!DOCTYPE html>
<html lang="en">
@ -94,7 +93,7 @@
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
{% if server_restart_required and not demo_mode %}
{% if server_restart_required %}
<div id='alert-restart-server' class='alert alert-danger' role='alert'>
<span class='fas fa-server'></span>
<strong>{% trans "Server Restart Required" %}</strong>

View File

@ -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 += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`;
} else {
html += `
<a href='#' pk='${row.pk}' class='load-sub-assembly' id='load-sub-assembly-${row.pk}'>
<span class='fas fa-sync-alt' title='{% trans "Load BOM for subassembly" %}'></span>
</a> `;
}
}
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 = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream float-right'></span>`;
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);
}
}
},
});

View File

@ -264,7 +264,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
var html = `<div class='btn-group float-right' role='group'>`;
// 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 += ` <small>({% trans "Batch" %}: ${row.batch})</small>`;
}
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 `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`;
if (bom_items.length == 0) {
return `<div id='output-progress-${row.pk}'><em><small>{% trans "No tracked BOM items for this build" %}</small></em></div>`;
}
var progressBar = makeProgressBar(
countAllocatedLines(row),
bom_items.length,
{
max_width: '150px',
}
);
return `<div id='output-progress-${row.pk}'>${progressBar}</div>`;
},
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 `<em><small>{% trans "No required tests for this build" %}</small></em>`;
}
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 `<em>{% trans "loading" %}...</em><span class='fas fa-spinner fa-spin float-right'></span>`;
}
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 = `<div class='btn-group float-right' role='group'>`;
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 = `
<tr id='allocation_row_${pk}' class='part-allocation-row'>
<tr id='items_${pk}' class='part-allocation-row'>
<td id='part_${pk}'>
${thumb} ${sub_part.full_name}
</td>
@ -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(

View File

@ -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';

View File

@ -113,8 +113,6 @@ function renderStockItem(name, data, parameters={}, options={}) {
}
}
var html = `
<span>
${part_detail}
@ -146,7 +144,7 @@ function renderStockLocation(name, data, parameters={}, options={}) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'><small>{% trans "Location ID" %}: ${data.pk}</small></span>`;
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 += `<span><b>${data.reference}</b></span> - ${data.quantity} x ${data.part_detail.full_name}`;
html += `<span class='float-right'><small>{% trans "Build ID" %}: ${data.pk}</span></span>`;
html += `<span><b>${data.reference}</b> - ${data.quantity} x ${data.part_detail.full_name}</span>`;
html += `<p><i>${data.title}</i></p>`;
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 = `
<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>
<span class='float-right'>
<small>{% trans "Shipment ID" %}: ${data.pk}</small>
</span>
`;
var html = `<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>`;
html += renderId('{% trans "Shipment ID" %}', data.pk, parameters);
return html;
}
@ -323,7 +317,7 @@ function renderPartCategory(name, data, parameters={}, options={}) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'><small>{% trans "Category ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Category ID" %}', data.pk, parameters);
return html;
}
@ -366,7 +360,7 @@ function renderManufacturerPart(name, data, parameters={}, options={}) {
html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'><small>{% trans "Manufacturer Part ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Manufacturer Part ID" %}', data.pk, parameters);
return html;
}
@ -395,9 +389,7 @@ function renderSupplierPart(name, data, parameters={}, options={}) {
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'><small>{% trans "Supplier Part ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);
return html;
}

View File

@ -500,6 +500,11 @@ function duplicateBom(part_id, options={}) {
*/
function partStockLabel(part, options={}) {
// Prevent literal string 'null' from being displayed
if (part.units == null) {
part.units = '';
}
if (part.in_stock) {
// There IS stock available for this part

View File

@ -6,9 +6,10 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'STICKY_HEADER' user=request.user as sticky %}
{% navigation_enabled as plugin_nav %}
{% inventree_demo_mode as demo %}
{% inventree_show_about user as show_about %}
{% inventree_customize 'navbar_message' as navbar_message %}
{% inventree_customize 'hide_admin_link' as hide_admin_link %}
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
<div class="container-fluid">
@ -89,7 +90,7 @@
{% if navbar_message %}
{% include "spacer.html" %}
<div class='flex justify-content-center'>
{{ navbar_message }}
{{ navbar_message | safe }}
</div>
{% include "spacer.html" %}
{% include "spacer.html" %}
@ -132,7 +133,7 @@
</a>
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
{% if user.is_authenticated %}
{% if user.is_staff and not demo %}
{% if user.is_staff and not hide_admin_link %}
<li><a class='dropdown-item' href="{% url 'admin:index' %}"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
{% endif %}
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>

View File

@ -87,31 +87,4 @@
<!-- TODO - Enumerate system issues here! -->
{% endfor %}
{% endif %}
<tr>
<td colspan='3'><strong>{% trans "Parts" %}</strong></td>
</tr>
<tr>
<td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Part Categories" %}</td>
<td>{{ part_cat_count }}</td>
</tr>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Parts" %}</td>
<td>{{ part_count }}</td>
</tr>
<tr>
<td colspan="3"><strong>{% trans "Stock Items" %}</strong></td>
</tr>
<tr>
<td><span class='fas fa-map-marker-alt'></span></td>
<td>{% trans "Stock Locations" %}</td>
<td>{{ stock_loc_count }}</td>
</tr>
<tr>
<td><span class='fas fa-boxes'></span></td>
<td>{% trans "Stock Items" %}</td>
<td>{{ stock_item_count }}</td>
</tr>
</table>