2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 19:20:55 +00:00

Merge branch 'master' into stock-item-forms

# Conflicts:
#	InvenTree/stock/serializers.py
#	InvenTree/stock/templates/stock/item_base.html
This commit is contained in:
Oliver
2021-11-01 12:00:40 +11:00
261 changed files with 72226 additions and 5878 deletions

View File

@ -27,7 +27,7 @@ function loadAttachmentTable(url, options) {
return '{% trans "No attachments found" %}';
},
sortable: true,
search: false,
search: true,
queryParams: options.filters || {},
onPostBody: function() {
// Add callback for 'edit' button
@ -58,12 +58,16 @@ function loadAttachmentTable(url, options) {
var fn = value.toLowerCase();
if (fn.endsWith('.pdf')) {
if (fn.endsWith('.csv')) {
icon = 'fa-file-csv';
} else if (fn.endsWith('.pdf')) {
icon = 'fa-file-pdf';
} else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) {
icon = 'fa-file-excel';
} else if (fn.endsWith('.doc') || fn.endsWith('.docx')) {
icon = 'fa-file-word';
} else if (fn.endsWith('.zip') || fn.endsWith('.7z')) {
icon = 'fa-file-archive';
} else {
var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif'];

View File

@ -407,7 +407,7 @@ function loadBomTable(table, options) {
// 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>`;
var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream float-right'></span>`;
html += renderLink(text, `/part/${row.sub_part}/bom/`);
}
@ -470,7 +470,7 @@ function loadBomTable(table, options) {
var text = value;
if (value == null || value <= 0) {
text = `<span class='label label-warning'>{% trans "No Stock" %}</span>`;
text = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
}
return renderLink(text, url);
@ -612,7 +612,7 @@ function loadBomTable(table, options) {
var bValidate = makeIconButton('fa-check-circle icon-green', 'bom-validate-button', row.pk, '{% trans "Validate BOM Item" %}');
var bValid = `<span title='{% trans "This line has been validated" %}' class='fas fa-check-double icon-green'/>`;
var bValid = makeIconButton('fa-check-double icon-green', 'bom-valid-button', row.pk, '{% trans "This line has been validated" %}', {disabled: true});
var bSubs = makeIconButton('fa-exchange-alt icon-blue', 'bom-substitutes-button', row.pk, '{% trans "Edit substitute parts" %}');

View File

@ -34,8 +34,8 @@ function buildFormFields() {
reference: {
prefix: global_settings.BUILDORDER_REFERENCE_PREFIX,
},
title: {},
part: {},
title: {},
quantity: {},
parent: {
filters: {
@ -937,7 +937,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var progress = makeProgressBar(
allocatedLines,
totalLines
totalLines,
{
max_width: '150px',
}
);
build_progress.html(progress);
@ -1404,6 +1407,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(

View File

@ -325,15 +325,15 @@ function loadCompanyTable(table, url, options={}) {
var html = imageHoverIcon(row.image) + renderLink(value, row.url);
if (row.is_customer) {
html += `<span title='{% trans "Customer" %}' class='fas fa-user-tie label-right'></span>`;
html += `<span title='{% trans "Customer" %}' class='fas fa-user-tie float-right'></span>`;
}
if (row.is_manufacturer) {
html += `<span title='{% trans "Manufacturer" %}' class='fas fa-industry label-right'></span>`;
html += `<span title='{% trans "Manufacturer" %}' class='fas fa-industry float-right'></span>`;
}
if (row.is_supplier) {
html += `<span title='{% trans "Supplier" %}' class='fas fa-building label-right'></span>`;
html += `<span title='{% trans "Supplier" %}' class='fas fa-building float-right'></span>`;
}
return html;
@ -493,15 +493,15 @@ function loadManufacturerPartTable(table, url, options) {
var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url);
if (row.part_detail.is_template) {
html += `<span class='fas fa-clone label-right' title='{% trans "Template part" %}'></span>`;
html += `<span class='fas fa-clone float-right' title='{% trans "Template part" %}'></span>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`;
html += `<span class='fas fa-tools float-right' title='{% trans "Assembled part" %}'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
html += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Inactive" %}</span>`;
}
return html;
@ -750,15 +750,15 @@ function loadSupplierPartTable(table, url, options) {
var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url);
if (row.part_detail.is_template) {
html += `<span class='fas fa-clone label-right' title='{% trans "Template part" %}'></span>`;
html += `<span class='fas fa-clone float-right' title='{% trans "Template part" %}'></span>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`;
html += `<span class='fas fa-tools float-right' title='{% trans "Assembled part" %}'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
html += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Inactive" %}</span>`;
}
return html;

View File

@ -281,18 +281,22 @@ function setupFilterList(tableKey, table, target) {
// One blank slate, please
element.empty();
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`);
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`);
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// If there are no filters defined for this table, exit now
if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
return;
}
// If there are filters currently "in use", add them in!
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-default filter-tag'><span class='fas fa-filter'></span></button>`);
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`);
if (Object.keys(filters).length > 0) {
element.append(`<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-default filter-tag'><span class='fas fa-backspace icon-red'></span></button>`);
element.append(`<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`);
}
for (var key in filters) {
@ -303,11 +307,6 @@ function setupFilterList(tableKey, table, target) {
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
}
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() {
@ -320,7 +319,7 @@ function setupFilterList(tableKey, table, target) {
html += generateAvailableFilterList(tableKey);
html += generateFilterInput(tableKey);
html += `<button title='{% trans "Create filter" %}' class='btn btn-default filter-tag' id='${make}'><span class='fas fa-plus'></span></button>`;
html += `<button title='{% trans "Create filter" %}' class='btn btn-outline-secondary filter-button' id='${make}'><span class='fas fa-plus'></span></button>`;
element.append(html);

View File

@ -2,7 +2,6 @@
{% load inventree_extras %}
/* globals
attachToggle,
createNewModal,
inventreeFormDataUpload,
inventreeGet,
@ -49,6 +48,9 @@
*
*/
// Set global default theme for select2
$.fn.select2.defaults.set('theme', 'bootstrap-5');
/*
* Return true if the OPTIONS specify that the user
* can perform a GET method at the endpoint.
@ -531,11 +533,6 @@ function constructFormBody(fields, options) {
// Attach clear callbacks (if required)
addClearCallbacks(fields, options);
attachToggle(modal);
$(modal + ' .select2-container').addClass('select-full-width');
$(modal + ' .select2-container').css('width', '100%');
modalShowSubmitButton(modal, true);
$(modal).on('click', '#modal-form-submit', function() {
@ -575,13 +572,14 @@ function insertConfirmButton(options) {
var message = options.confirmMessage || '{% trans "Confirm" %}';
var confirm = `
<span style='float: left;'>
${message}
<input id='modal-confirm' name='confirm' type='checkbox'>
</span>`;
var html = `
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="modal-confirm">
<label class="form-check-label" for="modal-confirm">${message}</label>
</div>
`;
$(options.modal).find('#modal-footer-buttons').append(confirm);
$(options.modal).find('#modal-footer-buttons').append(html);
// Disable the 'submit' button
$(options.modal).find('#modal-form-submit').prop('disabled', true);
@ -633,6 +631,10 @@ function submitFormData(fields, options) {
var has_files = false;
var data_valid = true;
var data_errors = {};
// Extract values for each field
for (var idx = 0; idx < options.field_names.length; idx++) {
@ -645,6 +647,21 @@ function submitFormData(fields, options) {
if (field) {
switch (field.type) {
// Ensure numerical fields are "valid"
case 'integer':
case 'float':
case 'decimal':
if (!validateFormField(name, options)) {
data_valid = false;
data_errors[name] = ['{% trans "Enter a valid number" %}'];
}
break;
default:
break;
}
var value = getFormFieldValue(name, field, options);
// Handle file inputs
@ -674,6 +691,11 @@ function submitFormData(fields, options) {
}
}
if (!data_valid) {
handleFormErrors(data_errors, fields, options);
return;
}
var upload_func = inventreePut;
if (has_files) {
@ -744,7 +766,8 @@ function updateFieldValues(fields, options) {
* Update the value of a named field
*/
function updateFieldValue(name, value, field, options) {
var el = $(options.modal).find(`#id_${name}`);
var el = getFormFieldElement(name, options);
if (!el) {
console.log(`WARNING: updateFieldValue could not find field '${name}'`);
@ -772,6 +795,46 @@ function updateFieldValue(name, value, field, options) {
}
// Find the named field element in the modal DOM
function getFormFieldElement(name, options) {
var el = $(options.modal).find(`#id_${name}`);
if (!el.exists) {
console.log(`ERROR: Could not find form element for field '${name}'`);
}
return el;
}
/*
* Check that a "numerical" input field has a valid number in it.
* An invalid number is expunged at the client side by the getFormFieldValue() function,
* which means that an empty string '' is sent to the server if the number is not valud.
* This can result in confusing error messages displayed under the form field.
*
* So, we can invalid numbers and display errors *before* the form is submitted!
*/
function validateFormField(name, options) {
if (getFormFieldElement(name, options)) {
var el = document.getElementById(`id_${name}`);
if (el.validity.valueMissing) {
// Accept empty strings (server will validate)
return true;
} else {
return el.validity.valid;
}
} else {
return false;
}
}
/*
* Extract and field value before sending back to the server
*
@ -783,7 +846,7 @@ function updateFieldValue(name, value, field, options) {
function getFormFieldValue(name, field, options) {
// Find the HTML element
var el = $(options.modal).find(`#id_${name}`);
var el = getFormFieldElement(name, options);
if (!el) {
return null;
@ -877,7 +940,7 @@ function clearFormErrors(options) {
$(options.modal).find('.form-error-message').remove();
// Remove the "has error" class
$(options.modal).find('.has-error').removeClass('has-error');
$(options.modal).find('.form-field-error').removeClass('form-field-error');
// Hide the 'non field errors'
$(options.modal).find('#non-field-errors').html('');
@ -1050,8 +1113,8 @@ function handleFormErrors(errors, fields, options) {
*/
function addFieldErrorMessage(field_name, error_text, error_idx, options) {
// Add the 'has-error' class
$(options.modal).find(`#div_id_${field_name}`).addClass('has-error');
// Add the 'form-field-error' class
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
var field_dom = $(options.modal).find(`#errors-${field_name}`);
@ -1098,7 +1161,9 @@ function addFieldCallbacks(fields, options) {
function addFieldCallback(name, field, options) {
$(options.modal).find(`#id_${name}`).change(function() {
var el = getFormFieldElement(name, options);
el.change(function() {
var value = getFormFieldValue(name, field, options);
@ -1244,7 +1309,7 @@ function addSecondaryModal(field, fields, options) {
var html = `
<span style='float: right;'>
<div type='button' class='btn btn-primary btn-secondary' title='${secondary.title || secondary.label}' id='btn-new-${name}'>
<div type='button' class='btn btn-primary btn-secondary btn-form-secondary' title='${secondary.title || secondary.label}' id='btn-new-${name}'>
${secondary.label || secondary.title}
</div>
</span>`;
@ -1311,7 +1376,7 @@ function initializeRelatedField(field, fields, options) {
}
// Find the select element and attach a select2 to it
var select = $(options.modal).find(`#id_${name}`);
var select = getFormFieldElement(name, options);
// Add a button to launch a 'secondary' modal
if (field.secondary != null) {
@ -1438,6 +1503,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 || '';
}
@ -1499,7 +1569,7 @@ function initializeRelatedField(field, fields, options) {
*/
function setRelatedFieldData(name, data, options) {
var select = $(options.modal).find(`#id_${name}`);
var select = getFormFieldElement(name, options);
var option = new Option(name, data.pk, true, true);
@ -1520,14 +1590,11 @@ function setRelatedFieldData(name, data, options) {
function initializeChoiceField(field, fields, options) {
var name = field.name;
var select = $(options.modal).find(`#id_${name}`);
var select = getFormFieldElement(field.name, options);
select.select2({
dropdownAutoWidth: false,
dropdownParent: $(options.modal),
width: '100%',
});
}
@ -1673,7 +1740,7 @@ function constructField(name, parameters, options) {
<div class='panel-heading form-panel-heading' id='form-panel-heading-${group}'>`;
if (group_options.collapsible) {
html += `
<div data-toggle='collapse' data-target='#form-panel-content-${group}'>
<div data-bs-toggle='collapse' data-bs-target='#form-panel-content-${group}'>
<a href='#'><span id='group-icon-${group}' class='fas fa-angle-up'></span>
`;
} else {
@ -1699,7 +1766,7 @@ function constructField(name, parameters, options) {
var form_classes = 'form-group';
if (parameters.errors) {
form_classes += ' has-error';
form_classes += ' form-field-error';
}
// Optional content to render before the field
@ -1741,7 +1808,7 @@ function constructField(name, parameters, options) {
html += `<div class='input-group'>`;
if (parameters.prefix) {
html += `<span class='input-group-addon'>${parameters.prefix}</span>`;
html += `<span class='input-group-text'>${parameters.prefix}</span>`;
}
}
@ -1751,7 +1818,7 @@ function constructField(name, parameters, options) {
if (!parameters.required) {
html += `
<span class='input-group-addon form-clear' id='clear_${name}' title='{% trans "Clear input" %}'>
<span class='input-group-text form-clear' id='clear_${name}' title='{% trans "Clear input" %}'>
<span class='icon-red fas fa-backspace'></span>
</span>`;
}
@ -1760,7 +1827,11 @@ function constructField(name, parameters, options) {
}
if (parameters.help_text && !options.hideLabels) {
html += constructHelpText(name, parameters, options);
// Boolean values are handled differently!
if (parameters.type != 'boolean') {
html += constructHelpText(name, parameters, options);
}
}
// Div for error messages
@ -1933,12 +2004,29 @@ function constructInputOptions(name, classes, type, parameters) {
opts.push(`placeholder='${parameters.placeholder}'`);
}
if (parameters.type == 'boolean') {
opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`);
switch (parameters.type) {
case 'boolean':
break;
case 'integer':
case 'float':
case 'decimal':
opts.push(`step='any'`);
break;
default:
break;
}
if (parameters.multiline) {
return `<textarea ${opts.join(' ')}></textarea>`;
} else if (parameters.type == 'boolean') {
return `
<div class='form-check form-switch'>
<input ${opts.join(' ')}>
<label class='form-check-label' for=''>
<em><small>${parameters.help_text}</small></em>
</label>
</div>
`;
} else {
return `<input ${opts.join(' ')}>`;
}
@ -1962,7 +2050,7 @@ function constructCheckboxInput(name, parameters) {
return constructInputOptions(
name,
'checkboxinput',
'form-check-input',
'checkbox',
parameters
);
@ -2047,7 +2135,7 @@ function constructChoiceInput(name, parameters) {
*/
function constructRelatedFieldInput(name) {
var html = `<select id='id_${name}' class='select form-control' name='${name}' style='width: 100%;'></select>`;
var html = `<select id='id_${name}' class='select form-control' name='${name}'></select>`;
// Don't load any options - they will be filled via an AJAX request
@ -2121,13 +2209,7 @@ function constructRawInput(name, parameters) {
*/
function constructHelpText(name, parameters) {
var style = '';
if (parameters.type == 'boolean') {
style = `style='display: inline-block; margin-left: 25px' `;
}
var html = `<div id='hint_id_${name}' ${style}class='help-block'><i>${parameters.help_text}</i></div>`;
var html = `<div id='hint_id_${name}' class='help-block'><i>${parameters.help_text}</i></div>`;
return html;
}

View File

@ -16,9 +16,9 @@
function yesNoLabel(value) {
if (value) {
return `<span class='label label-green'>{% trans "YES" %}</span>`;
return `<span class='badge rounded-pill bg-success'>{% trans "YES" %}</span>`;
} else {
return `<span class='label label-yellow'>{% trans "NO" %}</span>`;
return `<span class='badge rounded-pill bg-warning'>{% trans "NO" %}</span>`;
}
}
@ -92,7 +92,7 @@ function select2Thumbnail(image) {
*/
function makeIconBadge(icon, title) {
var html = `<span class='fas ${icon} label-right' title='${title}'></span>`;
var html = `<span class='icon-badge fas ${icon} float-right' title='${title}'></span>`;
return html;
}
@ -103,7 +103,7 @@ function makeIconBadge(icon, title) {
*/
function makeIconButton(icon, cls, pk, title, options={}) {
var classes = `btn btn-default btn-glyph ${cls}`;
var classes = `btn btn-outline-secondary ${cls}`;
var id = `${cls}-${pk}`;
@ -182,8 +182,14 @@ function makeProgressBar(value, maximum, opts={}) {
var id = options.id || 'progress-bar';
var style = '';
if (opts.max_width) {
style += `max-width: ${options.max_width}; `;
}
return `
<div id='${id}' class='progress'>
<div id='${id}' class='progress' style='${style}'>
<div class='progress-bar ${extraclass}' role='progressbar' aria-valuenow='${percent}' aria-valuemin='0' aria-valuemax='100' style='width:${percent}%'></div>
<div class='progress-value'>${text}</div>
</div>

View File

@ -47,14 +47,12 @@ function createNewModal(options={}) {
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label='{% trans "Close" %}'>
<span aria-hidden="true">&times;</span>
</button>
<h3 id='modal-title'>
<h4 id='modal-title' class='modal-title'>
<!-- Form title to be injected here -->
</h3>
</h4>
<button type='button' class='btn-close' data-bs-dismiss='modal' aria-label='{% trans "Close" %}'></button>
</div>
<div class='modal-form-content-wrapper'>
<div class='modal-body modal-form-content-wrapper'>
<div id='non-field-errors'>
<!-- Form error messages go here -->
</div>
@ -73,7 +71,8 @@ function createNewModal(options={}) {
<div id='modal-footer-buttons'>
<!-- Extra buttons can be inserted here -->
</div>
<button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Cancel" %}</button>
<span class='flex-item' style='flex-grow: 1;'></span>
<button type='button' class='btn btn-secondary' id='modal-form-close' data-bs-dismiss='modal'>{% trans "Cancel" %}</button>
<button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button>
</div>
</div>
@ -355,22 +354,6 @@ function partialMatcher(params, data) {
}
function attachToggle(modal) {
/* Attach 'bootstrap-toggle' functionality to any checkbox in the modal.
* This is simple for visual improvement,
* and also larger toggle style buttons are easier to press!
*/
$(modal).find(`input[type='checkbox']`).each(function() {
$(this).bootstrapToggle({
size: 'small',
onstyle: 'success',
offstyle: 'warning',
});
});
}
function attachSelect(modal) {
/* Attach 'select2' functionality to any drop-down list in the modal.
* Provides search filtering for dropdown items
@ -550,14 +533,14 @@ function renderErrorMessage(xhr) {
html += `
<div class='panel-group'>
<div class='panel panel-default'>
<div class='panel'>
<div class='panel panel-heading'>
<div class='panel-title'>
<a data-toggle='collapse' href="#collapse-error-info">{% trans "Show Error Information" %}</a>
<a data-bs-toggle='collapse' href="#collapse-error-info">{% trans "Show Error Information" %}</a>
</div>
</div>
<div class='panel-collapse collapse' id='collapse-error-info'>
<div class='panel-body'>`;
<div class='panel-content'>`;
html += xhr.responseText;
@ -698,7 +681,6 @@ function injectModalForm(modal, form_html) {
*/
$(modal).find('.modal-form-content').html(form_html);
attachSelect(modal);
attachToggle(modal);
}
@ -806,7 +788,7 @@ function insertActionButton(modal, options) {
if (already_present == false) {
var html = `
<span style='float: right;'>
<button name='${options.name}' type='submit' class='btn btn-default modal-form-button' value='${options.name}'>
<button name='${options.name}' type='submit' class='btn btn-outline-secondary modal-form-button' value='${options.name}'>
${options.title}
</button>
</span>`;

View File

@ -94,7 +94,7 @@ function renderStockItem(name, data, parameters, options) {
if (data.serial && data.quantity == 1) {
stock_detail = `{% trans "Serial Number" %}: ${data.serial}`;
} else if (data.quantity == 0) {
stock_detail = `<span class='label-form label-red'>{% trans "No Stock"% }</span>`;
stock_detail = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock"% }</span>`;
} else {
stock_detail = `{% trans "Quantity" %}: ${data.quantity}`;
}
@ -172,7 +172,7 @@ function renderPart(name, data, parameters, options) {
}
if (!data.active) {
extra += `<span class='label-form label-red'>{% trans "Inactive" %}</span>`;
extra += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Inactive" %}</span>`;
}
html += `

View File

@ -84,6 +84,7 @@ function createPurchaseOrder(options={}) {
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
supplier: {
icon: 'fa-building',
value: options.supplier,
secondary: {
title: '{% trans "Add Supplier" %}',
@ -864,6 +865,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
},
{
sortable: true,
switchable: false,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
@ -879,18 +881,29 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
field: 'purchase_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
return row.purchase_price_string || row.purchase_price;
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.purchase_price_currency
}
);
return formatter.format(row.purchase_price);
}
},
{
field: 'total_price',
sortable: true,
field: 'total_price',
title: '{% trans "Total price" %}',
title: '{% trans "Total Price" %}',
formatter: function(value, row) {
var total = row.purchase_price * row.quantity;
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
return formatter.format(total);
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.purchase_price_currency
}
);
return formatter.format(row.purchase_price * row.quantity);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
@ -1436,7 +1449,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
switchable: false,
switchable: true,
},
{
sortable: true,
@ -1456,14 +1469,6 @@ function loadSalesOrderLineItemTable(table, options={}) {
field: 'sale_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
return row.sale_price_string || row.sale_price;
}
},
{
sortable: true,
title: '{% trans "Total price" %}',
formatter: function(value, row) {
var total = row.sale_price * row.quantity;
var formatter = new Intl.NumberFormat(
'en-US',
{
@ -1472,7 +1477,23 @@ function loadSalesOrderLineItemTable(table, options={}) {
}
);
return formatter.format(total);
return formatter.format(row.sale_price);
}
},
{
field: 'total_price',
sortable: true,
title: '{% trans "Total Price" %}',
formatter: function(value, row) {
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.sale_price_currency
}
);
return formatter.format(row.sale_price * row.quantity);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
@ -1544,6 +1565,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
if (pending) {
columns.push({
field: 'buttons',
switchable: false,
formatter: function(value, row, index, field) {
var html = `<div class='btn-group float-right' role='group'>`;
@ -1641,6 +1663,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 +1683,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 +1798,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

@ -410,14 +410,12 @@ function toggleStar(options) {
}
function partStockLabel(part, options={}) {
var label_class = options.label_class || 'label-form';
function partStockLabel(part) {
if (part.in_stock) {
return `<span class='label ${label_class} label-green'>{% trans "Stock" %}: ${part.in_stock}</span>`;
return `<span class='badge rounded-pill bg-success'>{% trans "Stock" %}: ${part.in_stock}</span>`;
} else {
return `<span class='label ${label_class} label-red'>{% trans "No Stock" %}</span>`;
return `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
}
}
@ -453,7 +451,7 @@ function makePartIcons(part) {
}
if (!part.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
html += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Inactive" %}</span>`;
}
return html;
@ -530,7 +528,7 @@ function loadPartVariantTable(table, partId, options={}) {
}
if (!row.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
html += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Inactive" %}</span>`;
}
return html;
@ -769,14 +767,10 @@ function partGridTile(part) {
// Rows for table view
var rows = '';
if (part.IPN) {
rows += `<tr><td><b>{% trans "IPN" %}</b></td><td>${part.IPN}</td></tr>`;
}
var stock = `${part.in_stock}`;
if (!part.in_stock) {
stock = `<span class='label label-red'>{% trans "No Stock" %}</label>`;
stock = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
}
rows += `<tr><td><b>{% trans "Stock" %}</b></td><td>${stock}</td></tr>`;
@ -791,8 +785,8 @@ function partGridTile(part) {
var html = `
<div class='product-card card'>
<div class='panel panel-default panel-inventree product-card-panel'>
<div class='card product-card borderless'>
<div class='panel product-card-panel'>
<div class='panel-heading'>
<a href='/part/${part.pk}/'>
<b>${part.full_name}</b>
@ -949,20 +943,20 @@ function loadPartTable(table, url, options={}) {
// Is stock "low" (below the 'minimum_stock' quantity)?
if (row.minimum_stock && row.minimum_stock > value) {
value += `<span class='label label-right label-warning'>{% trans "Low stock" %}</span>`;
value += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Low stock" %}</span>`;
}
} else if (row.on_order) {
// There is no stock available, but stock is on order
value = `0<span class='label label-right label-primary'>{% trans "On Order" %}: ${row.on_order}</span>`;
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.on_order}</span>`;
link = '?display=purchase-orders';
} else if (row.building) {
// There is no stock available, but stock is being built
value = `0<span class='label label-right label-info'>{% trans "Building" %}: ${row.building}</span>`;
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
link = '?display=build-orders';
} else {
// There is no stock available
value = `0<span class='label label-right label-danger'>{% trans "No Stock" %}</span>`;
value = `0<span class='badge badge-right rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
}
return renderLink(value, `/part/${row.pk}/${link}`);
@ -989,6 +983,8 @@ function loadPartTable(table, url, options={}) {
}
});
var grid_view = inventreeLoad('part-grid-view') == 1;
$(table).inventreeTable({
url: url,
method: 'get',
@ -1003,8 +999,52 @@ function loadPartTable(table, url, options={}) {
},
columns: columns,
showColumns: true,
showCustomView: false,
showCustomView: grid_view,
showCustomViewButton: false,
onPostBody: function() {
grid_view = inventreeLoad('part-grid-view') == 1;
if (grid_view) {
$('#view-part-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-part-grid').removeClass('btn-outline-secondary').addClass('btn-secondary');
} else {
$('#view-part-grid').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
}
},
buttons: [
{
icon: 'fas fa-bars',
attributes: {
title: '{% trans "Display as list" %}',
id: 'view-part-list',
},
event: () => {
inventreeSave('part-grid-view', 0);
$(table).bootstrapTable(
'refreshOptions',
{
showCustomView: false,
}
);
}
},
{
icon: 'fas fa-th',
attributes: {
title: '{% trans "Display as grid" %}',
id: 'view-part-grid',
},
event: () => {
inventreeSave('part-grid-view', 1);
$(table).bootstrapTable(
'refreshOptions',
{
showCustomView: true,
}
);
}
}
],
customView: function(data) {
var html = '';
@ -1117,14 +1157,88 @@ function loadPartCategoryTable(table, options) {
setupFilterList(filterKey, table, filterListElement);
var tree_view = inventreeLoad('category-tree-view') == 1;
table.inventreeTable({
treeEnable: tree_view,
rootParentId: options.params.parent,
uniqueId: 'pk',
idField: 'pk',
treeShowField: 'name',
parentIdField: 'parent',
method: 'get',
url: options.url || '{% url "api-part-category-list" %}',
queryParams: filters,
sidePagination: 'server',
disablePagination: tree_view,
sidePagination: tree_view ? 'client' : 'server',
serverSort: !tree_view,
search: !tree_view,
name: 'category',
original: original,
showColumns: true,
buttons: [
{
icon: 'fas fa-bars',
attributes: {
title: '{% trans "Display as list" %}',
id: 'view-category-list',
},
event: () => {
inventreeSave('category-tree-view', 0);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: false,
serverSort: true,
search: true,
pagination: true,
}
);
}
},
{
icon: 'fas fa-sitemap',
attributes: {
title: '{% trans "Display as tree" %}',
id: 'view-category-tree',
},
event: () => {
inventreeSave('category-tree-view', 1);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: true,
serverSort: false,
search: false,
pagination: false,
}
);
}
}
],
onPostBody: function() {
tree_view = inventreeLoad('category-tree-view') == 1;
if (tree_view) {
$('#view-category-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-tree').removeClass('btn-outline-secondary').addClass('btn-secondary');
table.treegrid({
treeColumn: 0,
onChange: function() {
table.bootstrapTable('resetView');
},
onExpand: function() {
}
});
} else {
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
}
},
columns: [
{
checkbox: true,
@ -1154,7 +1268,8 @@ function loadPartCategoryTable(table, options) {
{
field: 'pathstring',
title: '{% trans "Path" %}',
switchable: true,
switchable: !tree_view,
visible: !tree_view,
sortable: false,
},
{

View File

@ -544,17 +544,17 @@ function removeStockRow(e) {
}
function passFailBadge(result, align='float-right') {
function passFailBadge(result) {
if (result) {
return `<span class='label label-green ${align}'>{% trans "PASS" %}</span>`;
return `<span class='badge badge-right rounded-pill bg-success'>{% trans "PASS" %}</span>`;
} else {
return `<span class='label label-red ${align}'>{% trans "FAIL" %}</span>`;
return `<span class='badge badge-right rounded-pill bg-danger'>{% trans "FAIL" %}</span>`;
}
}
function noResultBadge(align='float-right') {
return `<span class='label label-blue ${align}'>{% trans "NO RESULT" %}</span>`;
function noResultBadge() {
return `<span class='badge badge-right rounded-pill bg-info'>{% trans "NO RESULT" %}</span>`;
}
function formatDate(row) {
@ -562,11 +562,7 @@ function formatDate(row) {
var html = row.date;
if (row.user_detail) {
html += `<span class='badge'>${row.user_detail.username}</span>`;
}
if (row.attachment) {
html += `<a href='${row.attachment}'><span class='fas fa-file-alt label-right'></span></a>`;
html += `<span class='badge badge-right rounded-pill bg-secondary'>${row.user_detail.username}</span>`;
}
return html;
@ -647,6 +643,15 @@ function loadStockTestResultsTable(table, options) {
{
field: 'value',
title: '{% trans "Value" %}',
formatter: function(value, row) {
var html = value;
if (row.attachment) {
html += `<a href='${row.attachment}'><span class='fas fa-file-alt float-right'></span></a>`;
}
return html;
}
},
{
field: 'notes',
@ -972,16 +977,12 @@ function loadStockTable(table, options) {
}
if (row.quantity <= 0) {
html += `<span class='label label-right label-danger'>{% trans "Depleted" %}</span>`;
html += `<span class='badge rounded-pill bg-danger'>{% trans "Depleted" %}</span>`;
}
return html;
}
};
if (!options.params.ordering) {
col['sortable'] = true;
}
columns.push(col);
@ -1536,7 +1537,19 @@ function loadStockLocationTable(table, options) {
filters[key] = params[key];
}
var tree_view = inventreeLoad('location-tree-view') == 1;
table.inventreeTable({
treeEnable: tree_view,
rootParentId: options.params.parent,
uniqueId: 'pk',
idField: 'pk',
treeShowField: 'name',
parentIdField: 'parent',
disablePagination: tree_view,
sidePagination: tree_view ? 'client' : 'server',
serverSort: !tree_view,
search: !tree_view,
method: 'get',
url: options.url || '{% url "api-location-list" %}',
queryParams: filters,
@ -1544,6 +1557,69 @@ function loadStockLocationTable(table, options) {
name: 'location',
original: original,
showColumns: true,
onPostBody: function() {
tree_view = inventreeLoad('location-tree-view') == 1;
if (tree_view) {
$('#view-location-list').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-tree').removeClass('btn-outline-secondary').addClass('btn-secondary');
table.treegrid({
treeColumn: 1,
onChange: function() {
table.bootstrapTable('resetView');
},
onExpand: function() {
}
});
} else {
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
}
},
buttons: [
{
icon: 'fas fa-bars',
attributes: {
title: '{% trans "Display as list" %}',
id: 'view-location-list',
},
event: () => {
inventreeSave('location-tree-view', 0);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: false,
serverSort: true,
search: true,
pagination: true,
}
);
}
},
{
icon: 'fas fa-sitemap',
attributes: {
title: '{% trans "Display as tree" %}',
id: 'view-location-tree',
},
event: () => {
inventreeSave('location-tree-view', 1);
table.bootstrapTable(
'refreshOptions',
{
treeEnable: true,
serverSort: false,
search: false,
pagination: false,
}
);
}
}
],
columns: [
{
checkbox: true,
@ -1778,8 +1854,8 @@ function loadStockTrackingTable(table, options) {
formatter: function(value, row, index, field) {
// Manually created entries can be edited or deleted
if (false && !row.system) {
var bEdit = "<button title='{% trans 'Edit tracking entry' %}' class='btn btn-entry-edit btn-default btn-glyph' type='button' url='/stock/track/" + row.pk + "/edit/'><span class='fas fa-edit'/></button>";
var bDel = "<button title='{% trans 'Delete tracking entry' %}' class='btn btn-entry-delete btn-default btn-glyph' type='button' url='/stock/track/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'/></button>";
var bEdit = "<button title='{% trans 'Edit tracking entry' %}' class='btn btn-entry-edit btn-outline-secondary' type='button' url='/stock/track/" + row.pk + "/edit/'><span class='fas fa-edit'/></button>";
var bDel = "<button title='{% trans 'Delete tracking entry' %}' class='btn btn-entry-delete btn-outline-secondary' type='button' url='/stock/track/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'/></button>";
return "<div class='btn-group' role='group'>" + bEdit + bDel + "</div>";
} else {

View File

@ -214,6 +214,8 @@ $.fn.inventreeTable = function(options) {
options.pageList = [25, 50, 100, 250, 'all'];
options.totalField = 'count';
options.dataField = 'results';
} else {
options.pagination = false;
}
// Extract query params
@ -397,3 +399,8 @@ function customGroupSorter(sortName, sortOrder, sortData) {
$.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['en-US-custom']);
})(jQuery);
$.extend($.fn.treegrid.defaults, {
expanderExpandedClass: 'treegrid-expander-expanded',
expanderCollapsedClass: 'treegrid-expander-collapsed'
});