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