2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Merge branch 'inventree:master' into fix-html-tags

This commit is contained in:
Matthias Mair
2022-02-12 00:32:26 +01:00
committed by GitHub
77 changed files with 27099 additions and 23718 deletions

View File

@ -15,6 +15,7 @@
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %}
{% include "InvenTree/settings/setting.html" with key="PART_NAME_FORMAT" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_HISTORY" icon="fa-history" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_BOM" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}

View File

@ -16,10 +16,13 @@
{% endif %}
</td>
<td><strong>{{ setting.name }}</strong></td>
<td>
{{ setting.description }}
</td>
<td>
{% if setting.is_bool %}
<div class='form-check form-switch'>
<input class='form-check-input' fieldname='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' disabled='' {% if setting.as_bool %}checked=''{% endif %}>
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %} {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
</div>
{% else %}
<div id='setting-{{ setting.pk }}'>
@ -31,16 +34,12 @@
{% endif %}
</span>
{{ setting.units }}
<div class='btn-group float-right'>
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
<span class='fas fa-edit icon-green'></span>
</button>
</div>
</div>
{% endif %}
<td>
{{ setting.description }}
</td>
<td>
<div class='btn-group float-right'>
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
<span class='fas fa-edit icon-green'></span>
</button>
</div>
</td>
</tr>
</tr>

View File

@ -62,6 +62,43 @@
{% block js_ready %}
{{ block.super }}
// Callback for when boolean settings are edited
$('table').find('.boolean-setting').change(function() {
var setting = $(this).attr('setting');
var pk = $(this).attr('pk');
var plugin = $(this).attr('plugin');
var user = $(this).attr('user');
var checked = this.checked;
// Global setting by default
var url = `/api/settings/global/${pk}/`;
if (plugin) {
url = `/api/plugin/settings/${pk}/`;
} else if (user) {
url = `/api/settings/user/${pk}/`;
}
inventreePut(
url,
{
value: checked.toString(),
},
{
method: 'PATCH',
onSuccess: function(data) {
},
error: function(xhr) {
showApiError(xhr, url);
}
}
);
});
// Callback for when non-boolean settings are edited
$('table').find('.btn-edit-setting').click(function() {
var setting = $(this).attr('setting');
var pk = $(this).attr('pk');

View File

@ -15,6 +15,7 @@
*/
/* exported
constructBomUploadTable,
downloadBomTemplate,
exportBom,
newPartFromBomWizard,
@ -22,8 +23,221 @@
loadUsedInTable,
removeRowFromBomWizard,
removeColFromBomWizard,
submitBomTable
*/
/* Construct a table of data extracted from a BOM file.
* This data is used to import a BOM interactively.
*/
function constructBomUploadTable(data, options={}) {
if (!data.rows) {
// TODO: Error message!
return;
}
function constructRow(row, idx, fields) {
// Construct an individual row from the provided data
var errors = {};
if (data.errors && data.errors.length > idx) {
errors = data.errors[idx];
}
var field_options = {
hideLabels: true,
hideClearButton: true,
form_classes: 'bom-form-group',
};
function constructRowField(field_name) {
var field = fields[field_name] || null;
if (!field) {
return `Cannot render field '${field_name}`;
}
field.value = row[field_name];
return constructField(`items_${field_name}_${idx}`, field, field_options);
}
// Construct form inputs
var sub_part = constructRowField('sub_part');
var quantity = constructRowField('quantity');
var reference = constructRowField('reference');
var overage = constructRowField('overage');
var variants = constructRowField('allow_variants');
var inherited = constructRowField('inherited');
var optional = constructRowField('optional');
var note = constructRowField('note');
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeIconButton('fa-info-circle', 'button-row-data', idx, '{% trans "Display row data" %}');
buttons += makeIconButton('fa-times icon-red', 'button-row-remove', idx, '{% trans "Remove row" %}');
buttons += `</div>`;
var html = `
<tr id='items_${idx}' class='bom-import-row' idx='${idx}'>
<td id='col_sub_part_${idx}'>${sub_part}</td>
<td id='col_quantity_${idx}'>${quantity}</td>
<td id='col_reference_${idx}'>${reference}</td>
<td id='col_overage_${idx}'>${overage}</td>
<td id='col_variants_${idx}'>${variants}</td>
<td id='col_inherited_${idx}'>${inherited}</td>
<td id='col_optional_${idx}'>${optional}</td>
<td id='col_note_${idx}'>${note}</td>
<td id='col_buttons_${idx}'>${buttons}</td>
</tr>`;
$('#bom-import-table tbody').append(html);
// Handle any errors raised by initial data import
if (errors.part) {
addFieldErrorMessage(`items_sub_part_${idx}`, errors.part);
}
if (errors.quantity) {
addFieldErrorMessage(`items_quantity_${idx}`, errors.quantity);
}
// Initialize the "part" selector for this row
initializeRelatedField(
{
name: `items_sub_part_${idx}`,
value: row.part,
api_url: '{% url "api-part-list" %}',
filters: {
component: true,
},
model: 'part',
required: true,
auto_fill: false,
onSelect: function(data, field, opts) {
// TODO?
},
}
);
// Add callback for "remove row" button
$(`#button-row-remove-${idx}`).click(function() {
$(`#items_${idx}`).remove();
});
// Add callback for "show data" button
$(`#button-row-data-${idx}`).click(function() {
var modal = createNewModal({
title: '{% trans "Row Data" %}',
cancelText: '{% trans "Close" %}',
hideSubmitButton: true
});
// Prettify the original import data
var pretty = JSON.stringify(row, undefined, 4);
var html = `
<div class='alert alert-block'>
<pre><code>${pretty}</code></pre>
</div>`;
modalSetContent(modal, html);
$(modal).modal('show');
});
}
// Request API endpoint options
getApiEndpointOptions('{% url "api-bom-list" %}', function(response) {
var fields = response.actions.POST;
data.rows.forEach(function(row, idx) {
constructRow(row, idx, fields);
});
});
}
/* Extract rows from the BOM upload table,
* and submit data to the server
*/
function submitBomTable(part_id, options={}) {
// Extract rows from the form
var rows = [];
var idx_values = [];
var url = '{% url "api-bom-upload" %}';
$('.bom-import-row').each(function() {
var idx = $(this).attr('idx');
idx_values.push(idx);
// Extract each field from the row
rows.push({
part: part_id,
sub_part: getFormFieldValue(`items_sub_part_${idx}`, {}),
quantity: getFormFieldValue(`items_quantity_${idx}`, {}),
reference: getFormFieldValue(`items_reference_${idx}`, {}),
overage: getFormFieldValue(`items_overage_${idx}`, {}),
allow_variants: getFormFieldValue(`items_allow_variants_${idx}`, {type: 'boolean'}),
inherited: getFormFieldValue(`items_inherited_${idx}`, {type: 'boolean'}),
optional: getFormFieldValue(`items_optional_${idx}`, {type: 'boolean'}),
note: getFormFieldValue(`items_note_${idx}`, {}),
});
});
var data = {
items: rows,
};
var options = {
nested: {
items: idx_values,
}
};
getApiEndpointOptions(url, function(response) {
var fields = response.actions.POST;
// Disable the "Submit BOM" button
$('#bom-submit').prop('disabled', true);
$('#bom-submit-icon').show();
inventreePut(url, data, {
method: 'POST',
success: function(response) {
window.location.href = `/part/${part_id}/?display=bom`;
},
error: function(xhr) {
switch (xhr.status) {
case 400:
handleFormErrors(xhr.responseJSON, fields, options);
break;
default:
showApiError(xhr, url);
break;
}
// Re-enable the submit button
$('#bom-submit').prop('disabled', false);
$('#bom-submit-icon').hide();
}
});
});
}
function downloadBomTemplate(options={}) {
var format = options.format;
@ -77,7 +291,7 @@ function exportBom(part_id, options={}) {
value: inventreeLoad('bom-export-format', 'csv'),
choices: exportFormatOptions(),
},
cascading: {
cascade: {
label: '{% trans "Cascading" %}',
help_text: '{% trans "Download cascading / multi-level BOM" %}',
type: 'boolean',
@ -118,7 +332,7 @@ function exportBom(part_id, options={}) {
onSubmit: function(fields, opts) {
// Extract values from the form
var field_names = ['format', 'cascading', 'levels', 'parameter_data', 'stock_data', 'manufacturer_data', 'supplier_data'];
var field_names = ['format', 'cascade', 'levels', 'parameter_data', 'stock_data', 'manufacturer_data', 'supplier_data'];
var url = `/part/${part_id}/bom-download/?`;
@ -319,7 +533,19 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
rows += renderSubstituteRow(sub);
});
var part_thumb = thumbnailImage(options.sub_part_detail.thumbnail || options.sub_part_detail.image);
var part_name = options.sub_part_detail.full_name;
var part_desc = options.sub_part_detail.description;
var html = `
<div class='alert alert-block'>
<strong>{% trans "Base Part" %}</strong><hr>
${part_thumb} ${part_name} - <em>${part_desc}</em>
</div>
`;
// Add a table of individual rows
html += `
<table class='table table-striped table-condensed' id='substitute-table'>
<thead>
<tr>
@ -337,7 +563,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
html += `
<div class='alert alert-success alert-block'>
{% trans "Select and add a new variant item using the input below" %}
{% trans "Select and add a new substitute part using the input below" %}
</div>
`;
@ -766,6 +992,11 @@ function loadBomTable(table, options={}) {
// This function may be called recursively for multi-level BOMs
function requestSubItems(bom_pk, part_pk) {
// TODO: 2022-02-03 Currently, multi-level BOMs are not actually displayed.
// Re-enable this function once multi-level display has been re-deployed
return;
inventreeGet(
options.bom_url,
{
@ -945,7 +1176,9 @@ function loadBomTable(table, options={}) {
subs,
{
table: table,
part: row.part,
sub_part: row.sub_part,
sub_part_detail: row.sub_part_detail,
}
);
});

View File

@ -417,6 +417,145 @@ function completeBuildOutputs(build_id, outputs, options={}) {
}
/**
* Launch a modal form to delete selected build outputs
*/
function deleteBuildOutputs(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}/delete-outputs/`, {
method: 'POST',
preFormContent: html,
fields: {},
confirm: true,
title: '{% trans "Delete 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) {
var data = {
outputs: [],
};
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);
}
});
opts.nested = {
'outputs': output_pk_values,
};
inventreePut(
opts.url,
data,
{
method: 'POST',
success: function(response) {
$(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, opts.url);
break;
}
}
}
);
}
});
}
/**
* Load a table showing all the BuildOrder allocations for a given part
*/
@ -594,6 +733,7 @@ function loadBuildOutputTable(build_info, options={}) {
{
success: function() {
$(table).bootstrapTable('refresh');
$('#build-stock-table').bootstrapTable('refresh');
}
}
);
@ -603,15 +743,17 @@ function loadBuildOutputTable(build_info, options={}) {
$(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/`,
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
deleteBuildOutputs(
build_info.pk,
[
output,
],
{
data: {
output: pk
},
onSuccess: function() {
success: function() {
$(table).bootstrapTable('refresh');
$('#build-stock-table').bootstrapTable('refresh');
}
}
);

View File

@ -837,7 +837,15 @@ function getFormFieldElement(name, options) {
var field_name = getFieldName(name, options);
var el = $(options.modal).find(`#id_${field_name}`);
var el = null;
if (options && options.modal) {
// Field element is associated with a model?
el = $(options.modal).find(`#id_${field_name}`);
} else {
// Field element is top-level
el = $(`#id_${field_name}`);
}
if (!el.exists) {
console.log(`ERROR: Could not find form element for field '${name}'`);
@ -882,12 +890,13 @@ function validateFormField(name, options) {
* - field: The field specification provided from the OPTIONS request
* - options: The original options object provided by the client
*/
function getFormFieldValue(name, field, options) {
function getFormFieldValue(name, field={}, options={}) {
// Find the HTML element
var el = getFormFieldElement(name, options);
if (!el) {
console.log(`ERROR: getFormFieldValue could not locate field '{name}'`);
return null;
}
@ -973,16 +982,22 @@ function handleFormSuccess(response, options) {
/*
* Remove all error text items from the form
*/
function clearFormErrors(options) {
function clearFormErrors(options={}) {
// Remove the individual error messages
$(options.modal).find('.form-error-message').remove();
if (options && options.modal) {
// Remove the individual error messages
$(options.modal).find('.form-error-message').remove();
// Remove the "has error" class
$(options.modal).find('.form-field-error').removeClass('form-field-error');
// Remove the "has error" class
$(options.modal).find('.form-field-error').removeClass('form-field-error');
// Hide the 'non field errors'
$(options.modal).find('#non-field-errors').html('');
// Hide the 'non field errors'
$(options.modal).find('#non-field-errors').html('');
} else {
$('.form-error-message').remove();
$('.form-field-errors').removeClass('form-field-error');
$('#non-field-errors').html('');
}
}
/*
@ -1010,7 +1025,7 @@ function clearFormErrors(options) {
*
*/
function handleNestedErrors(errors, field_name, options) {
function handleNestedErrors(errors, field_name, options={}) {
var error_list = errors[field_name];
@ -1041,8 +1056,31 @@ function handleNestedErrors(errors, field_name, options) {
// Here, error_item is a map of field names to error messages
for (sub_field_name in error_item) {
var errors = error_item[sub_field_name];
if (sub_field_name == 'non_field_errors') {
var row = null;
if (options.modal) {
row = $(options.modal).find(`#items_${nest_id}`);
} else {
row = $(`#items_${nest_id}`);
}
for (var ii = errors.length - 1; ii >= 0; ii--) {
var html = `
<div id='error_${ii}_non_field_error' class='help-block form-field-error form-error-message'>
<strong>${errors[ii]}</strong>
</div>`;
row.after(html);
}
}
// Find the target (nested) field
var target = `${field_name}_${sub_field_name}_${nest_id}`;
@ -1066,15 +1104,23 @@ function handleNestedErrors(errors, field_name, options) {
* - fields: The form data object
* - options: Form options provided by the client
*/
function handleFormErrors(errors, fields, options) {
function handleFormErrors(errors, fields={}, options={}) {
// Reset the status of the "submit" button
$(options.modal).find('#modal-form-submit').prop('disabled', false);
if (options.modal) {
$(options.modal).find('#modal-form-submit').prop('disabled', false);
}
// Remove any existing error messages from the form
clearFormErrors(options);
var non_field_errors = $(options.modal).find('#non-field-errors');
var non_field_errors = null;
if (options.modal) {
non_field_errors = $(options.modal).find('#non-field-errors');
} else {
non_field_errors = $('#non-field-errors');
}
// TODO: Display the JSON error text when hovering over the "info" icon
non_field_errors.append(
@ -1150,16 +1196,21 @@ function handleFormErrors(errors, fields, options) {
/*
* Add a rendered error message to the provided field
*/
function addFieldErrorMessage(name, error_text, error_idx, options) {
function addFieldErrorMessage(name, error_text, error_idx=0, options={}) {
field_name = getFieldName(name, options);
// Add the 'form-field-error' class
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
var field_dom = null;
var field_dom = $(options.modal).find(`#errors-${field_name}`);
if (options && options.modal) {
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
field_dom = $(options.modal).find(`#errors-${field_name}`);
} else {
$(`#div_id_${field_name}`).addClass('form-field-error');
field_dom = $(`#errors-${field_name}`);
}
if (field_dom) {
if (field_dom.exists()) {
var error_html = `
<span id='error_${error_idx}_id_${field_name}' class='help-block form-error-message'>
@ -1228,12 +1279,18 @@ function addClearCallbacks(fields, options) {
}
function addClearCallback(name, field, options) {
function addClearCallback(name, field, options={}) {
var field_name = getFieldName(name, options);
var el = $(options.modal).find(`#clear_${field_name}`);
var el = null;
if (options && options.modal) {
el = $(options.modal).find(`#clear_${field_name}`);
} else {
el = $(`#clear_${field_name}`);
}
if (!el) {
console.log(`WARNING: addClearCallback could not find field '${name}'`);
return;
@ -1330,11 +1387,13 @@ function hideFormGroup(group, options) {
$(options.modal).find(`#form-panel-${group}`).hide();
}
// Show a form group
function showFormGroup(group, options) {
$(options.modal).find(`#form-panel-${group}`).show();
}
function setFormGroupVisibility(group, vis, options) {
if (vis) {
showFormGroup(group, options);
@ -1344,7 +1403,7 @@ function setFormGroupVisibility(group, vis, options) {
}
function initializeRelatedFields(fields, options) {
function initializeRelatedFields(fields, options={}) {
var field_names = options.field_names;
@ -1452,12 +1511,11 @@ function addSecondaryModal(field, fields, options) {
* - field: Field definition from the OPTIONS request
* - options: Original options object provided by the client
*/
function initializeRelatedField(field, fields, options) {
function initializeRelatedField(field, fields, options={}) {
var name = field.name;
if (!field.api_url) {
// TODO: Provide manual api_url option?
console.log(`WARNING: Related field '${name}' missing 'api_url' parameter.`);
return;
}
@ -1475,10 +1533,22 @@ function initializeRelatedField(field, fields, options) {
// limit size for AJAX requests
var pageSize = options.pageSize || 25;
var parent = null;
var auto_width = false;
var width = '100%';
// Special considerations if the select2 input is a child of a modal
if (options && options.modal) {
parent = $(options.modal);
auto_width = true;
width = null;
}
select.select2({
placeholder: '',
dropdownParent: $(options.modal),
dropdownAutoWidth: false,
dropdownParent: parent,
dropdownAutoWidth: auto_width,
width: width,
language: {
noResults: function(query) {
if (field.noResults) {
@ -1654,7 +1724,7 @@ function initializeRelatedField(field, fields, options) {
* - data: JSON data representing the model instance
* - options: The modal form specifications
*/
function setRelatedFieldData(name, data, options) {
function setRelatedFieldData(name, data, options={}) {
var select = getFormFieldElement(name, options);
@ -1734,6 +1804,9 @@ function renderModelData(name, model, data, parameters, options) {
case 'partparametertemplate':
renderer = renderPartParameterTemplate;
break;
case 'purchaseorder':
renderer = renderPurchaseOrder;
break;
case 'salesorder':
renderer = renderSalesOrder;
break;
@ -1776,10 +1849,10 @@ function renderModelData(name, model, data, parameters, options) {
/*
* Construct a field name for the given field
*/
function getFieldName(name, options) {
function getFieldName(name, options={}) {
var field_name = name;
if (options.depth) {
if (options && options.depth) {
field_name += `_${options.depth}`;
}
@ -1869,18 +1942,24 @@ function constructField(name, parameters, options) {
options.current_group = group;
}
var form_classes = 'form-group';
var form_classes = options.form_classes || 'form-group';
if (parameters.errors) {
form_classes += ' form-field-error';
}
// Optional content to render before the field
if (parameters.before) {
html += parameters.before;
}
html += `<div id='div_id_${field_name}' class='${form_classes}'>`;
var hover_title = '';
if (parameters.help_text) {
hover_title = ` title='${parameters.help_text}'`;
}
html += `<div id='div_id_${field_name}' class='${form_classes}' ${hover_title}>`;
// Add a label
if (!options.hideLabels) {
@ -1922,7 +2001,7 @@ function constructField(name, parameters, options) {
if (extra) {
if (!parameters.required) {
if (!parameters.required && !options.hideClearButton) {
html += `
<span class='input-group-text form-clear' id='clear_${field_name}' title='{% trans "Clear input" %}'>
<span class='icon-red fas fa-backspace'></span>
@ -2050,7 +2129,7 @@ function constructInput(name, parameters, options) {
// Construct a set of default input options which apply to all input types
function constructInputOptions(name, classes, type, parameters) {
function constructInputOptions(name, classes, type, parameters, options={}) {
var opts = [];
@ -2132,11 +2211,18 @@ function constructInputOptions(name, classes, type, parameters) {
if (parameters.multiline) {
return `<textarea ${opts.join(' ')}></textarea>`;
} else if (parameters.type == 'boolean') {
var help_text = '';
if (!options.hideLabels && parameters.help_text) {
help_text = `<em><small>${parameters.help_text}</small></em>`;
}
return `
<div class='form-check form-switch'>
<input ${opts.join(' ')}>
<label class='form-check-label' for=''>
<em><small>${parameters.help_text}</small></em>
${help_text}
</label>
</div>
`;
@ -2159,13 +2245,14 @@ function constructHiddenInput(name, parameters) {
// Construct a "checkbox" input
function constructCheckboxInput(name, parameters) {
function constructCheckboxInput(name, parameters, options={}) {
return constructInputOptions(
name,
'form-check-input',
'checkbox',
parameters
parameters,
options
);
}

View File

@ -62,15 +62,16 @@ function imageHoverIcon(url) {
* @param {String} url is the image URL
* @returns html <img> tag
*/
function thumbnailImage(url) {
function thumbnailImage(url, options={}) {
if (!url) {
url = blankImage();
}
// TODO: Support insertion of custom classes
var title = options.title || '';
var html = `<img class='hover-img-thumb' src='${url}'>`;
var html = `<img class='hover-img-thumb' src='${url}' title='${title}'>`;
return html;

View File

@ -161,7 +161,7 @@ function renderPart(name, data, parameters, options) {
html += ` <span>${data.full_name || data.name}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
html += ` - <i><small>${data.description}</small></i>`;
}
var extra = '';
@ -221,20 +221,54 @@ function renderOwner(name, data, parameters, options) {
}
// Renderer for "SalesOrder" model
// Renderer for "PurchaseOrder" model
// eslint-disable-next-line no-unused-vars
function renderSalesOrder(name, data, parameters, options) {
var html = `<span>${data.reference}</span>`;
function renderPurchaseOrder(name, data, parameters, options) {
var html = '';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
var thumbnail = null;
html += `<span>${prefix}${data.reference}</span>`;
if (data.supplier_detail) {
thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
html += ' - ' + select2Thumbnail(thumbnail);
html += `<span>${data.supplier_detail.name}</span>`;
}
if (data.description) {
html += ` - <i>${data.description}</i>`;
html += ` - <em>${data.description}</em>`;
}
html += `
<span class='float-right'>
<small>
{% trans "Order ID" %}: ${data.pk}
</small>
</small>
</span>
`;
return html;
}
// Renderer for "SalesOrder" model
// eslint-disable-next-line no-unused-vars
function renderSalesOrder(name, data, parameters, options) {
var html = `<span>${data.reference}</span>`;
if (data.description) {
html += ` - <em>${data.description}</em>`;
}
html += `
<span class='float-right'>
<small>
{% trans "Order ID" %}: ${data.pk}
</small>
</span>`;
return html;

View File

@ -47,6 +47,7 @@
exportStock,
findStockItemBySerialNumber,
loadInstalledInTable,
loadStockAllocationTable,
loadStockLocationTable,
loadStockTable,
loadStockTestResultsTable,
@ -2203,6 +2204,157 @@ function loadStockTable(table, options) {
}
/*
* Display a table of allocated stock, for either a part or stock item
* Allocations are displayed for:
*
* a) Sales Orders
* b) Build Orders
*/
function loadStockAllocationTable(table, options={}) {
var params = options.params || {};
params.build_detail = true;
var filterListElement = options.filterList || '#filter-list-allocations';
var filters = {};
var filterKey = options.filterKey || options.name || 'allocations';
var original = {};
for (var k in params) {
original[k] = params[k];
filters[k] = params[k];
}
setupFilterList(filterKey, table, filterListElement);
/*
* We have two separate API queries to make here:
* a) Build Order Allocations
* b) Sales Order Allocations
*
* We will let the call to inventreeTable take care of build orders,
* and then load sales orders after that.
*/
table.inventreeTable({
url: '{% url "api-build-item-list" %}',
name: 'allocations',
original: original,
method: 'get',
queryParams: filters,
sidePagination: 'client',
showColumns: false,
onLoadSuccess: function(tableData) {
var query_params = params;
query_params.customer_detail = true;
query_params.order_detail = true;
delete query_params.build_detail;
// Load sales order allocation data
inventreeGet('{% url "api-so-allocation-list" %}', query_params, {
success: function(data) {
// Update table to include sales order data
$(table).bootstrapTable('append', data);
}
});
},
columns: [
{
field: 'order',
title: '{% trans "Order" %}',
formatter: function(value, row) {
var html = '';
if (row.build) {
// Add an icon for the part being built
html += thumbnailImage(row.build_detail.part_detail.thumbnail, {
title: row.build_detail.part_detail.full_name
});
html += ' ';
html += renderLink(
global_settings.BUILDORDER_REFERENCE_PREFIX + row.build_detail.reference,
`/build/${row.build}/`
);
html += makeIconBadge('fa-tools', '{% trans "Build Order" %}');
} else if (row.order) {
// Add an icon for the customer
html += thumbnailImage(row.customer_detail.thumbnail || row.customer_detail.image, {
title: row.customer_detail.name,
});
html += ' ';
html += renderLink(
global_settings.SALESORDER_REFERENCE_PREFIX + row.order_detail.reference,
`/order/sales-order/${row.order}/`
);
html += makeIconBadge('fa-truck', '{% trans "Sales Order" %}');
} else {
return '-';
}
return html;
}
},
{
field: 'description',
title: '{% trans "Description" %}',
formatter: function(value, row) {
if (row.order_detail) {
return row.order_detail.description;
} else if (row.build_detail) {
return row.build_detail.title;
} else {
return '-';
}
}
},
{
field: 'status',
title: '{% trans "Order Status" %}',
formatter: function(value, row) {
if (row.build) {
return buildStatusDisplay(row.build_detail.status);
} else if (row.order) {
return salesOrderStatusDisplay(row.order_detail.status);
} else {
return '-';
}
}
},
{
field: 'quantity',
title: '{% trans "Allocated Quantity" %}',
formatter: function(value, row) {
var text = value;
var pk = row.stock_item || row.item;
if (pk) {
var url = `/stock/item/${pk}/`;
return renderLink(text, url);
} else {
return value;
}
}
},
]
});
}
/*
* Display a table of stock locations
*/
@ -2252,7 +2404,6 @@ function loadStockLocationTable(table, options) {
method: 'get',
url: options.url || '{% url "api-location-list" %}',
queryParams: filters,
sidePagination: 'server',
name: 'location',
original: original,
showColumns: true,