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:
@ -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>`
|
||||
|
@ -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>`
|
||||
|
@ -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" %}'>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
};
|
||||
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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}`;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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'>`;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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>
|
@ -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>
|
Reference in New Issue
Block a user