2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-19 21:45:39 +00:00

Merge branch 'inventree:master' into plugin-2037

This commit is contained in:
Matthias Mair
2021-10-20 23:52:51 +02:00
committed by GitHub
49 changed files with 1421 additions and 1451 deletions

View File

@ -12,12 +12,14 @@
<hr>
<div class='col-sm-3' id='item-panel'>
<ul class='list-group' id='action-item-list'>
</ul>
<div class='panel panel-default panel-inventree'>
<ul class='list-group' id='action-item-list'>
</ul>
</div>
</div>
<div class='col-sm-9' id='details-panel'>
<ul class='list-group' id='detail-item-list'>
<li class='list-group-item'>
<li class='list-group-item panel panel-default panel-inventree'>
<div class='container'>
<img class='index-bg' src='{% static "img/inventree.png" %}'>
</div>
@ -54,7 +56,7 @@ function addHeaderAction(label, title, icon, options) {
// Add a detail item to the detail item-panel
$("#detail-item-list").append(
`<li class='list-group-item' id='detail-${label}'>
`<li class='list-group-item panel panel-default panel-inventree' id='detail-${label}'>
<h4>${title}</h4>
<table class='table table-condensed table-striped' id='table-${label}'></table>
</li>`

View File

@ -26,12 +26,14 @@
{% endif %}
<div class='col-sm-3' id='item-panel'>
<ul class='list-group' id='search-item-list'>
</ul>
<div class='panel panel-default panel-inventree'>
<ul class='list-group' id='search-item-list'>
</ul>
</div>
</div>
<div class='col-sm-9' id='details-panel'>
<ul class='list-group' id='search-result-list'>
<li class='list-group-item'>
<li class='list-group-item panel panel-default panel-inventree'>
<div class='container'>
<img class='index-bg' src='{% static "img/inventree.png" %}'>
</div>
@ -67,7 +69,7 @@
// Add a results table
$('#search-result-list').append(
`<li class='list-group-item' id='search-result-${label}'>
`<li class='list-group-item panel panel-default panel-inventree' id='search-result-${label}'>
<h4>${title}</h4>
<table class='table table-condensed table-striped' id='table-${label}'></table>
</li>`

View File

@ -10,12 +10,12 @@
</li>
<li class='list-group-item'>
<strong>{% trans "User Settings" %}</strong>
<span class='fas fa-user'></span> <strong>{% trans "User Settings" %}</strong>
</li>
<li class='list-group-item' title='{% trans "Account" %}'>
<a href='#' class='nav-toggle' id='select-account'>
<span class='fas fa-user'></span> {% trans "Account" %}
<span class='fas fa-user-cog'></span> {% trans "Account" %}
</a>
</li>
@ -60,7 +60,7 @@
{% if user.is_staff %}
<li class='list-group-item'>
<strong>{% trans "InvenTree Settings" %}</strong>
<span class='fas fa-cogs'></span> <strong>{% trans "Global Settings" %}</strong>
</li>
<li class='list-group-item' title='{% trans "Server" %}'>

View File

@ -16,6 +16,7 @@
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %}
{% include "InvenTree/settings/setting.html" with key="SEARCH_SHOW_STOCK_LEVELS" user_setting=True icon='fa-boxes' %}
</tbody>
</table>
</div>

View File

@ -1,7 +0,0 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{% trans "Are you sure you want to delete this attachment?" %}
<br>
{% endblock %}

View File

@ -143,7 +143,6 @@
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>

View File

@ -1,23 +0,0 @@
{% block collapse_preamble %}
{% endblock %}
<div class='panel-group'>
<div class='panel panel-default'>
<div {% block collapse_panel_setup %}class='panel panel-heading'{% endblock %}>
<div class='row'>
<div class='col-sm-6'>
<div class='panel-title'>
<a data-toggle='collapse' href="#collapse-item-{{ collapse_id }}">{% block collapse_title %}Title{% endblock %}</a>
</div>
</div>
{% block collapse_heading %}
{% endblock %}
</div>
</div>
<div class='panel-collapse collapse' id='collapse-item-{{ collapse_id }}'>
<div class='panel-body'>
{% block collapse_content %}
{% endblock %}
</div>
</div>
</div>
</div>

View File

@ -1,19 +0,0 @@
{% block collapse_preamble %}
{% endblock %}
<div class='panel-group panel-index'>
<div class='panel panel-default'>
<div {% block collapse_panel_setup %}class='panel panel-heading'{% endblock %}>
<div class='panel-title'>
<a data-toggle='collapse' href="#collapse-item-{{ collapse_id }}">{% block collapse_title %}Title{% endblock %}</a>
</div>
{% block collapse_heading %}
{% endblock %}
</div>
<div class='panel-collapse collapse' id='collapse-item-{{ collapse_id }}'>
<div class='panel-body'>
{% block collapse_content %}
{% endblock %}
</div>
</div>
</div>
</div>

View File

@ -140,11 +140,13 @@ function inventreeDocReady() {
offset: 0
},
success: function(data) {
var transformed = $.map(data.results, function(el) {
return {
label: el.full_name,
id: el.pk,
thumbnail: el.thumbnail
thumbnail: el.thumbnail,
data: el,
};
});
response(transformed);
@ -164,7 +166,18 @@ function inventreeDocReady() {
html += `'> `;
html += item.label;
html += '</span></a>';
html += '</span>';
if (user_settings.SEARCH_SHOW_STOCK_LEVELS) {
html += partStockLabel(
item.data,
{
label_class: 'label-right',
}
);
}
html += '</a>';
return $('<li>').append(html).appendTo(ul);
};
@ -290,3 +303,8 @@ function loadBrandIcon(element, name) {
element.addClass('fab fa-' + name);
}
}
// Convenience function to determine if an element exists
$.fn.exists = function() {
return this.length !== 0;
};

View File

@ -3,6 +3,9 @@
/* exported
attachNavCallbacks,
enableNavbar,
initNavTree,
loadTree,
onPanelLoad,
*/
@ -113,3 +116,253 @@ function onPanelLoad(panel, callback) {
});
}
function loadTree(url, tree, options={}) {
/* Load the side-nav tree view
Args:
url: URL to request tree data
tree: html ref to treeview
options:
data: data object to pass to the AJAX request
selected: ID of currently selected item
name: name of the tree
*/
var data = {};
if (options.data) {
data = options.data;
}
var key = 'inventree-sidenav-items-';
if (options.name) {
key += options.name;
}
$.ajax({
url: url,
type: 'get',
dataType: 'json',
data: data,
success: function(response) {
if (response.tree) {
$(tree).treeview({
data: response.tree,
enableLinks: true,
showTags: true,
});
if (localStorage.getItem(key)) {
var saved_exp = localStorage.getItem(key).split(',');
// Automatically expand the desired notes
for (var q = 0; q < saved_exp.length; q++) {
$(tree).treeview('expandNode', parseInt(saved_exp[q]));
}
}
// Setup a callback whenever a node is toggled
$(tree).on('nodeExpanded nodeCollapsed', function(event, data) {
// Record the entire list of expanded items
var expanded = $(tree).treeview('getExpanded');
var exp = [];
for (var i = 0; i < expanded.length; i++) {
exp.push(expanded[i].nodeId);
}
// Save the expanded nodes
localStorage.setItem(key, exp);
});
}
},
error: function(xhr, ajaxOptions, thrownError) {
// TODO
}
});
}
/**
* Initialize navigation tree display
*/
function initNavTree(options) {
var resize = true;
if ('resize' in options) {
resize = options.resize;
}
var label = options.label || 'nav';
var stateLabel = `${label}-tree-state`;
var widthLabel = `${label}-tree-width`;
var treeId = options.treeId || '#sidenav-left';
var toggleId = options.toggleId;
// Initially hide the tree
$(treeId).animate({
width: '0px',
}, 0, function() {
if (resize) {
$(treeId).resizable({
minWidth: '0px',
maxWidth: '500px',
handles: 'e, se',
grid: [5, 5],
stop: function(event, ui) {
var width = Math.round(ui.element.width());
if (width < 75) {
$(treeId).animate({
width: '0px'
}, 50);
localStorage.setItem(stateLabel, 'closed');
} else {
localStorage.setItem(stateLabel, 'open');
localStorage.setItem(widthLabel, `${width}px`);
}
}
});
}
var state = localStorage.getItem(stateLabel);
var width = localStorage.getItem(widthLabel) || '300px';
if (state && state == 'open') {
$(treeId).animate({
width: width,
}, 50);
}
});
// Register callback for 'toggle' button
if (toggleId) {
$(toggleId).click(function() {
var state = localStorage.getItem(stateLabel) || 'closed';
var width = localStorage.getItem(widthLabel) || '300px';
if (state == 'open') {
$(treeId).animate({
width: '0px'
}, 50);
localStorage.setItem(stateLabel, 'closed');
} else {
$(treeId).animate({
width: width,
}, 50);
localStorage.setItem(stateLabel, 'open');
}
});
}
}
/**
* Handle left-hand icon menubar display
*/
function enableNavbar(options) {
var resize = true;
if ('resize' in options) {
resize = options.resize;
}
var label = options.label || 'nav';
label = `navbar-${label}`;
var stateLabel = `${label}-state`;
var widthLabel = `${label}-width`;
var navId = options.navId || '#sidenav-right';
var toggleId = options.toggleId;
// Extract the saved width for this element
$(navId).animate({
'width': '45px',
'min-width': '45px',
'display': 'block',
}, 50, function() {
// Make the navbar resizable
if (resize) {
$(navId).resizable({
minWidth: options.minWidth || '100px',
maxWidth: options.maxWidth || '500px',
handles: 'e, se',
grid: [5, 5],
stop: function(event, ui) {
// Record the new width
var width = Math.round(ui.element.width());
// Reasonably narrow? Just close it!
if (width <= 75) {
$(navId).animate({
width: '45px'
}, 50);
localStorage.setItem(stateLabel, 'closed');
} else {
localStorage.setItem(widthLabel, `${width}px`);
localStorage.setItem(stateLabel, 'open');
}
}
});
}
var state = localStorage.getItem(stateLabel);
var width = localStorage.getItem(widthLabel) || '250px';
if (state && state == 'open') {
$(navId).animate({
width: width
}, 100);
}
});
// Register callback for 'toggle' button
if (toggleId) {
$(toggleId).click(function() {
var state = localStorage.getItem(stateLabel) || 'closed';
var width = localStorage.getItem(widthLabel) || '250px';
if (state == 'open') {
$(navId).animate({
width: '45px',
minWidth: '45px',
}, 50);
localStorage.setItem(stateLabel, 'closed');
} else {
$(navId).animate({
'width': width
}, 50);
localStorage.setItem(stateLabel, 'open');
}
});
}
}

View File

@ -24,7 +24,7 @@
loadAllocationTable,
loadBuildOrderAllocationTable,
loadBuildOutputAllocationTable,
loadBuildPartsTable,
loadBuildOutputTable,
loadBuildTable,
*/
@ -108,126 +108,56 @@ function newBuildOrder(options={}) {
}
function makeBuildOutputActionButtons(output, buildInfo, lines) {
/* Generate action buttons for a build output.
*/
var buildId = buildInfo.pk;
var partId = buildInfo.part;
var outputId = 'untracked';
if (output) {
outputId = output.pk;
}
var panel = `#allocation-panel-${outputId}`;
function reloadTable() {
$(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh');
}
// Find the div where the buttons will be displayed
var buildActions = $(panel).find(`#output-actions-${outputId}`);
/*
* Construct a set of output buttons for a particular build output
*/
function makeBuildOutputButtons(output_id, build_info, options={}) {
var html = `<div class='btn-group float-right' role='group'>`;
if (lines > 0) {
html += makeIconButton(
'fa-sign-in-alt icon-blue', 'button-output-auto', outputId,
'{% trans "Allocate stock items to this build output" %}',
);
}
// Tracked parts? Must be individually allocated
if (build_info.tracked_parts) {
if (lines > 0) {
// Add a button to "cancel" the particular build output (unallocate)
// Add a button to allocate stock against this build output
html += makeIconButton(
'fa-minus-circle icon-red', 'button-output-unallocate', outputId,
'fa-sign-in-alt icon-blue',
'button-output-allocate',
output_id,
'{% trans "Allocate stock items to this build output" %}',
{
disabled: true,
}
);
// Add a button to unallocate stock from this build output
html += makeIconButton(
'fa-minus-circle icon-red',
'button-output-unallocate',
output_id,
'{% trans "Unallocate stock from build output" %}',
);
}
if (output) {
// Add a button to "complete" this build output
html += makeIconButton(
'fa-check-circle icon-green',
'button-output-complete',
output_id,
'{% trans "Complete build output" %}',
);
// Add a button to "complete" the particular build output
html += makeIconButton(
'fa-check icon-green', 'button-output-complete', outputId,
'{% trans "Complete build output" %}',
{
// disabled: true
}
);
// Add a button to "delete" this build output
html += makeIconButton(
'fa-trash-alt icon-red',
'button-output-delete',
output_id,
'{% trans "Delete build output" %}',
);
// Add a button to "delete" the particular build output
html += makeIconButton(
'fa-trash-alt icon-red', 'button-output-delete', outputId,
'{% trans "Delete build output" %}',
);
html += `</div>`;
// TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap)
}
return html;
html += '</div>';
buildActions.html(html);
// Add callbacks for the buttons
$(panel).find(`#button-output-auto-${outputId}`).click(function() {
var bom_items = $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('getData');
// Launch modal dialog to perform auto-allocation
allocateStockToBuild(
buildId,
partId,
bom_items,
{
source_location: buildInfo.source_location,
output: outputId,
success: reloadTable,
}
);
});
$(panel).find(`#button-output-complete-${outputId}`).click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/build/${buildId}/complete-output/`,
{
data: {
output: pk,
},
reload: true,
}
);
});
$(panel).find(`#button-output-unallocate-${outputId}`).click(function() {
var pk = $(this).attr('pk');
unallocateStock(buildId, {
output: pk,
table: table,
});
});
$(panel).find(`#button-output-delete-${outputId}`).click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/build/${buildId}/delete-output/`,
{
reload: true,
data: {
output: pk
}
}
);
});
}
@ -270,14 +200,160 @@ function unallocateStock(build_id, options={}) {
}
}
});
}
/**
* Launch a modal form to complete selected build outputs
*/
function completeBuildOutputs(build_id, outputs, options={}) {
if (outputs.length == 0) {
showAlertDialog(
'{% trans "Select Build Outputs" %}',
'{% trans "At least one build output must be selected" %}',
);
return;
}
// Render a single build output (StockItem)
function renderBuildOutput(output, opts={}) {
var pk = output.pk;
var output_html = imageHoverIcon(output.part_detail.thumbnail);
if (output.quantity == 1 && output.serial) {
output_html += `{% trans "Serial Number" %}: ${output.serial}`;
} else {
output_html += `{% trans "Quantity" %}: ${output.quantity}`;
}
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}');
buttons += '</div>';
var field = constructField(
`outputs_output_${pk}`,
{
type: 'raw',
html: output_html,
},
{
hideLabels: true,
}
);
var html = `
<tr id='output_row_${pk}'>
<td>${field}</td>
<td>${output.part_detail.full_name}</td>
<td>${buttons}</td>
</tr>`;
return html;
}
// Construct table entries
var table_entries = '';
outputs.forEach(function(output) {
table_entries += renderBuildOutput(output);
});
var html = `
<table class='table table-striped table-condensed' id='build-complete-table'>
<thead>
<th colspan='2'>{% trans "Output" %}</th>
<th><!-- Actions --></th>
</thead>
<tbody>
${table_entries}
</tbody>
</table>`;
constructForm(`/api/build/${build_id}/complete/`, {
method: 'POST',
preFormContent: html,
fields: {
status: {},
location: {},
},
confirm: true,
title: '{% trans "Complete Build Outputs" %}',
afterRender: function(fields, opts) {
// Setup callbacks to remove outputs
$(opts.modal).find('.button-row-remove').click(function() {
var pk = $(this).attr('pk');
$(opts.modal).find(`#output_row_${pk}`).remove();
});
},
onSubmit: function(fields, opts) {
// Extract data elements from the form
var data = {
outputs: [],
status: getFormFieldValue('status', {}, opts),
location: getFormFieldValue('location', {}, opts),
};
var output_pk_values = [];
outputs.forEach(function(output) {
var pk = output.pk;
var row = $(opts.modal).find(`#output_row_${pk}`);
if (row.exists()) {
data.outputs.push({
output: pk,
});
output_pk_values.push(pk);
}
});
// Provide list of nested values
opts.nested = {
'outputs': output_pk_values,
};
inventreePut(
opts.url,
data,
{
method: 'POST',
success: function(response) {
// Hide the modal
$(opts.modal).modal('hide');
if (options.success) {
options.success(response);
}
},
error: function(xhr) {
switch (xhr.status) {
case 400:
handleFormErrors(xhr.responseJSON, fields, opts);
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr);
break;
}
}
}
);
}
});
}
/**
* Load a table showing all the BuildOrder allocations for a given part
*/
function loadBuildOrderAllocationTable(table, options={}) {
/**
* Load a table showing all the BuildOrder allocations for a given part
*/
options.params['part_detail'] = true;
options.params['build_detail'] = true;
@ -357,17 +433,256 @@ function loadBuildOrderAllocationTable(table, options={}) {
}
function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
/*
* Display a "build output" table for a particular build.
*
* This displays a list of "active" (i.e. "in production") build outputs for a given build
*
*/
function loadBuildOutputTable(build_info, options={}) {
var table = options.table || '#build-output-table';
var params = options.params || {};
// Mandatory query filters
params.part_detail = true;
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) {
filters[key] = params[key];
}
// TODO: Initialize filter list
function setupBuildOutputButtonCallbacks() {
// Callback for the "allocate" button
$(table).find('.button-output-allocate').click(function() {
var pk = $(this).attr('pk');
// Find the "allocation" sub-table associated with this output
var subtable = $(`#output-sub-table-${pk}`);
if (subtable.exists()) {
var rows = subtable.bootstrapTable('getSelections');
// None selected? Use all!
if (rows.length == 0) {
rows = subtable.bootstrapTable('getData');
}
allocateStockToBuild(
build_info.pk,
build_info.part,
rows,
{
output: pk,
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
} else {
console.log(`WARNING: Could not locate sub-table for output ${pk}`);
}
});
// Callack for the "unallocate" button
$(table).find('.button-output-unallocate').click(function() {
var pk = $(this).attr('pk');
unallocateStock(build_info.pk, {
output: pk,
table: table
});
});
// Callback for the "complete" button
$(table).find('.button-output-complete').click(function() {
var pk = $(this).attr('pk');
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
completeBuildOutputs(
build_info.pk,
[
output,
],
{
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
// Callback for the "delete" button
$(table).find('.button-output-delete').click(function() {
var pk = $(this).attr('pk');
// TODO: Move this to the API
launchModalForm(
`/build/${build_info.pk}/delete-output/`,
{
data: {
output: pk
},
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
}
/*
* Load the "allocation table" for a particular build output.
*
* Args:
* - buildId: The PK of the Build object
* - partId: The PK of the Part object
* - output: The StockItem object which is the "output" of the build
* - options:
* -- table: The #id of the table (will be auto-calculated if not provided)
* Construct a "sub table" showing the required BOM items
*/
function constructBuildOutputSubTable(index, row, element) {
var sub_table_id = `output-sub-table-${row.pk}`;
var html = `
<div class='sub-table'>
<table class='table table-striped table-condensed' id='${sub_table_id}'></table>
</div>
`;
element.html(html);
loadBuildOutputAllocationTable(
build_info,
row,
{
table: `#${sub_table_id}`,
parent_table: table,
}
);
}
$(table).inventreeTable({
url: '{% url "api-stock-list" %}',
queryParams: filters,
original: params,
showColumns: false,
uniqueId: 'pk',
name: 'build-outputs',
sortable: true,
search: false,
sidePagination: 'server',
detailView: has_tracked_items,
detailFilter: function(index, row) {
return true;
},
detailFormatter: function(index, row, element) {
constructBuildOutputSubTable(index, row, element);
},
formatNoMatches: function() {
return '{% trans "No active build outputs found" %}';
},
onPostBody: function() {
// Add callbacks for the buttons
setupBuildOutputButtonCallbacks();
$(table).bootstrapTable('expandAllRows');
},
columns: [
{
title: '',
visible: true,
checkbox: true,
switchable: false,
},
{
field: 'part',
title: '{% trans "Part" %}',
formatter: function(value, row) {
var thumb = row.part_detail.thumbnail;
return imageHoverIcon(thumb) + row.part_detail.full_name + makePartIcons(row.part_detail);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
formatter: function(value, row) {
var url = `/stock/item/${row.pk}/`;
var text = '';
if (row.serial && row.quantity == 1) {
text = `{% trans "Serial Number" %}: ${row.serial}`;
} else {
text = `{% trans "Quantity" %}: ${row.quantity}`;
}
return renderLink(text, url);
}
},
{
field: 'allocated',
title: '{% trans "Allocated Parts" %}',
visible: has_tracked_items,
formatter: function(value, row) {
return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`;
}
},
{
field: 'actions',
title: '',
switchable: false,
formatter: function(value, row) {
return makeBuildOutputButtons(
row.pk,
build_info,
);
}
}
]
});
// Enable the "allocate" button when the sub-table is exanded
$(table).on('expand-row.bs.table', function(detail, index, row) {
$(`#button-output-allocate-${row.pk}`).prop('disabled', false);
});
// Disable the "allocate" button when the sub-table is collapsed
$(table).on('collapse-row.bs.table', function(detail, index, row) {
$(`#button-output-allocate-${row.pk}`).prop('disabled', true);
});
}
/*
* Display the "allocation table" for a particular build output.
*
* This displays a table of required allocations for a particular build output
*
* Args:
* - buildId: The PK of the Build object
* - partId: The PK of the Part object
* - output: The StockItem object which is the "output" of the build
* - options:
* -- table: The #id of the table (will be auto-calculated if not provided)
*/
function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var buildId = buildInfo.pk;
var partId = buildInfo.part;
@ -534,7 +849,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
},
name: 'build-allocation',
uniqueId: 'sub_part',
onPostBody: setupCallbacks,
search: options.search || false,
onPostBody: function(data) {
// Setup button callbacks
setupCallbacks();
},
onLoadSuccess: function(tableData) {
// Once the BOM data are loaded, request allocation data for this build output
@ -610,31 +929,31 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
$(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
}
// Update the total progress for this build output
var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`));
// Update the progress bar for this build output
var build_progress = $(`#output-progress-${outputId}`);
if (totalLines > 0) {
if (build_progress.exists()) {
if (totalLines > 0) {
var progress = makeProgressBar(
allocatedLines,
totalLines
);
buildProgress.html(progress);
var progress = makeProgressBar(
allocatedLines,
totalLines
);
build_progress.html(progress);
} else {
build_progress.html('');
}
} else {
buildProgress.html('');
console.log(`WARNING: Could not find progress bar for output ${outputId}`);
}
// Update the available actions for this build output
makeBuildOutputActionButtons(output, buildInfo, totalLines);
}
}
);
},
sortable: true,
showColumns: false,
detailViewByClick: true,
detailView: true,
detailFilter: function(index, row) {
return row.allocations != null;
@ -883,9 +1202,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
},
]
});
// Initialize the action buttons
makeBuildOutputActionButtons(output, buildInfo, 0);
}
@ -995,10 +1311,13 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
remaining = 0;
}
table_entries += renderBomItemRow(bom_item, remaining);
// We only care about entries which are not yet fully allocated
if (remaining > 0) {
table_entries += renderBomItemRow(bom_item, remaining);
}
}
if (bom_items.length == 0) {
if (table_entries.length == 0) {
showAlertDialog(
'{% trans "Select Parts" %}',
@ -1085,6 +1404,24 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
render_part_detail: true,
render_location_detail: true,
auto_fill: true,
onSelect: function(data, field, opts) {
// Adjust the 'quantity' field based on availability
if (!('quantity' in data)) {
return;
}
// Quantity remaining to be allocated
var remaining = Math.max((bom_item.required || 0) - (bom_item.allocated || 0), 0);
// Calculate the available quantity
var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0);
// Maximum amount that we need
var desired = Math.min(available, remaining);
updateFieldValue(`items_quantity_${bom_item.pk}`, desired, {}, opts);
},
adjustFilters: function(filters) {
// Restrict query to the selected location
var location = getFormFieldValue(
@ -1198,9 +1535,10 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
}
/*
* Display a table of Build orders
*/
function loadBuildTable(table, options) {
// Display a table of Build objects
var params = options.params || {};
@ -1467,190 +1805,4 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
}
});
});
}
function loadBuildPartsTable(table, options={}) {
/**
* Display a "required parts" table for build view.
*
* This is a simplified BOM view:
* - Does not display sub-bom items
* - Does not allow editing of BOM items
*
* Options:
*
* part: Part ID
* build: Build ID
* build_quantity: Total build quantity
* build_remaining: Number of items remaining
*/
// Query params
var params = {
sub_part_detail: true,
part: options.part,
};
var filters = {};
if (!options.disableFilters) {
filters = loadTableFilters('bom');
}
setupFilterList('bom', $(table));
for (var key in params) {
filters[key] = params[key];
}
function setupTableCallbacks() {
// Register button callbacks once the table data are loaded
// Callback for 'buy' button
$(table).find('.button-buy').click(function() {
var pk = $(this).attr('pk');
launchModalForm('{% url "order-parts" %}', {
data: {
parts: [
pk,
]
}
});
});
// Callback for 'build' button
$(table).find('.button-build').click(function() {
var pk = $(this).attr('pk');
newBuildOrder({
part: pk,
parent: options.build,
});
});
}
var columns = [
{
field: 'sub_part',
title: '{% trans "Part" %}',
switchable: false,
sortable: true,
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 sub_part = row.sub_part_detail;
html += makePartIcons(row.sub_part_detail);
// 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 label-right'></span>`;
html += renderLink(text, `/part/${row.sub_part}/bom/`);
}
return html;
}
},
{
field: 'sub_part_detail.description',
title: '{% trans "Description" %}',
},
{
field: 'reference',
title: '{% trans "Reference" %}',
searchable: true,
sortable: true,
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true
},
{
sortable: true,
switchable: false,
field: 'sub_part_detail.stock',
title: '{% trans "Available" %}',
formatter: function(value, row) {
return makeProgressBar(
value,
row.quantity * options.build_remaining,
{
id: `part-progress-${row.part}`
}
);
},
sorter: function(valA, valB, rowA, rowB) {
if (rowA.received == 0 && rowB.received == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(rowA.sub_part_detail.stock) / (rowA.quantity * options.build_remaining);
var progressB = parseFloat(rowB.sub_part_detail.stock) / (rowB.quantity * options.build_remaining);
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'actions',
title: '{% trans "Actions" %}',
switchable: false,
formatter: function(value, row) {
// Generate action buttons against the part
var html = `<div class='btn-group float-right' role='group'>`;
if (row.sub_part_detail.assembly) {
html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}');
}
if (row.sub_part_detail.purchaseable) {
html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}');
}
html += `</div>`;
return html;
}
}
];
table.inventreeTable({
url: '{% url "api-bom-list" %}',
showColumns: true,
name: 'build-parts',
sortable: true,
search: true,
onPostBody: setupTableCallbacks,
rowStyle: function(row) {
var classes = [];
// Shade rows differently if they are for different parent parts
if (row.part != options.part) {
classes.push('rowinherited');
}
if (row.validated) {
classes.push('rowvalid');
} else {
classes.push('rowinvalid');
}
return {
classes: classes.join(' '),
};
},
formatNoMatches: function() {
return '{% trans "No BOM items found" %}';
},
clickToSelect: true,
queryParams: filters,
original: params,
columns: columns,
});
}

View File

@ -1426,6 +1426,11 @@ function initializeRelatedField(field, fields, options) {
data = item.element.instance;
}
// Run optional callback function
if (field.onSelect && data) {
field.onSelect(data, field, options);
}
if (!data.pk) {
return field.placeholder || '';
}
@ -1843,6 +1848,8 @@ function constructInput(name, parameters, options) {
case 'candy':
func = constructCandyInput;
break;
case 'raw':
func = constructRawInput;
default:
// Unsupported field type!
break;
@ -2086,6 +2093,17 @@ function constructCandyInput(name, parameters) {
}
/*
* Construct a "raw" field input
* No actual field data!
*/
function constructRawInput(name, parameters) {
return parameters.html;
}
/*
* Construct a 'help text' div based on the field parameters
*

View File

@ -87,8 +87,10 @@ function select2Thumbnail(image) {
}
/*
* Construct an 'icon badge' which floats to the right of an object
*/
function makeIconBadge(icon, title) {
// Construct an 'icon badge' which floats to the right of an object
var html = `<span class='fas ${icon} label-right' title='${title}'></span>`;
@ -96,8 +98,10 @@ function makeIconBadge(icon, title) {
}
/*
* Construct an 'icon button' using the fontawesome set
*/
function makeIconButton(icon, cls, pk, title, options={}) {
// Construct an 'icon button' using the fontawesome set
var classes = `btn btn-default btn-glyph ${cls}`;

View File

@ -168,11 +168,7 @@ function renderPart(name, data, parameters, options) {
// Display available part quantity
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
if (data.in_stock == 0) {
extra += `<span class='label-form label-red'>{% trans "No Stock" %}</span>`;
} else {
extra += `<span class='label-form label-green'>{% trans "Stock" %}: ${data.in_stock}</span>`;
}
extra += partStockLabel(data);
}
if (!data.active) {

View File

@ -1641,6 +1641,13 @@ function loadSalesOrderLineItemTable(table, options={}) {
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
// Quantity remaining to be allocated
var remaining = (line_item.quantity || 0) - (line_item.allocated || 0);
if (remaining < 0) {
remaining = 0;
}
var fields = {
// SalesOrderLineItem reference
line: {
@ -1654,9 +1661,26 @@ function loadSalesOrderLineItemTable(table, options={}) {
in_stock: true,
part: line_item.part,
exclude_so_allocation: options.order,
}
},
auto_fill: true,
onSelect: function(data, field, opts) {
// Quantity available from this stock item
if (!('quantity' in data)) {
return;
}
// Calculate the available quantity
var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0);
// Maximum amount that we need
var desired = Math.min(available, remaining);
updateFieldValue('quantity', desired, {}, opts);
}
},
quantity: {
value: remaining,
},
};
@ -1752,7 +1776,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
showFooter: true,
uniqueId: 'pk',
detailView: show_detail,
detailViewByClick: show_detail,
detailViewByClick: false,
detailFilter: function(index, row) {
if (pending) {
// Order is pending

View File

@ -35,6 +35,7 @@
loadSellPricingChart,
loadSimplePartTable,
loadStockPricingChart,
partStockLabel,
toggleStar,
*/
@ -409,6 +410,18 @@ function toggleStar(options) {
}
function partStockLabel(part, options={}) {
var label_class = options.label_class || 'label-form';
if (part.in_stock) {
return `<span class='label ${label_class} label-green'>{% trans "Stock" %}: ${part.in_stock}</span>`;
} else {
return `<span class='label ${label_class} label-red'>{% trans "No Stock" %}</span>`;
}
}
function makePartIcons(part) {
/* Render a set of icons for the given part.
*/
@ -778,7 +791,7 @@ function partGridTile(part) {
var html = `
<div class='col-sm-3 card'>
<div class='product-card card'>
<div class='panel panel-default panel-inventree product-card-panel'>
<div class='panel-heading'>
<a href='/part/${part.pk}/'>
@ -1000,8 +1013,8 @@ function loadPartTable(table, url, options={}) {
data.forEach(function(row, index) {
// Force a new row every 4 columns, to prevent visual issues
if ((index > 0) && (index % 4 == 0) && (index < data.length)) {
// Force a new row every 5 columns
if ((index > 0) && (index % 5 == 0) && (index < data.length)) {
html += `</div><div class='row full-height'>`;
}

View File

@ -56,10 +56,10 @@ function enableButtons(elements, enabled) {
}
/* Link a bootstrap-table object to one or more buttons.
* The buttons will only be enabled if there is at least one row selected
*/
function linkButtonsToSelection(table, buttons) {
/* Link a bootstrap-table object to one or more buttons.
* The buttons will only be enabled if there is at least one row selected
*/
if (typeof table === 'string') {
table = $(table);

View File

@ -1,23 +0,0 @@
<table class='table table-striped table-condensed' id='{{ table_id }}'>
<tr>
<th data-field='part' data-sortable='true' data-searchable='true'>Part</th>
<th data-field='part' data-sortable='true' data-searchable='true'>Description</th>
<th data-field='part' data-sortable='true' data-searchable='true'>In Stock</th>
<th data-field='part' data-sortable='true' data-searchable='true'>On Order</th>
<th data-field='part' data-sortable='true' data-searchable='true'>Allocted</th>
<th data-field='part' data-sortable='true' data-searchable='true'>Net Stock</th>
</tr>
{% for part in parts %}
<tr>
<td>
{% include "hover_image.html" with image=part.image hover=True %}
<a href="{% url 'part-detail' part.id %}">{{ part.full_name }}</a>
</td>
<td>{{ part.description }}</td>
<td>{{ part.total_stock }}</td>
<td>{{ part.on_order }}</td>
<td>{{ part.allocation_count }}</td>
<td{% if part.net_stock < 0 %} class='red-cell'{% endif %}>{{ part.net_stock }}</td>
</tr>
{% endfor %}
</table>

View File

@ -1,3 +0,0 @@
<div>
<input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled or not roles.part.change %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off">
</div>