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

Split dynamic javascript files into two separate directories

- One gets translated and is served statically
- One does not get translated and is served dynamically
- Add CI step
This commit is contained in:
Oliver
2021-07-29 09:23:24 +10:00
parent bc3c3be751
commit 14aebfdae1
24 changed files with 177 additions and 23 deletions

View File

@ -0,0 +1,196 @@
{% load i18n %}
{% load inventree_extras %}
var jQuery = window.$;
$.urlParam = function(name){
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results==null) {
return null;
}
return decodeURI(results[1]) || 0;
}
// using jQuery
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function inventreeGet(url, filters={}, options={}) {
// Middleware token required for data update
//var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var csrftoken = getCookie('csrftoken');
return $.ajax({
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
type: 'GET',
data: filters,
dataType: 'json',
contentType: 'application/json',
success: function(response) {
if (options.success) {
options.success(response);
}
},
error: function(xhr, ajaxOptions, thrownError) {
console.error('Error on GET at ' + url);
console.error(thrownError);
if (options.error) {
options.error({
error: thrownError
});
}
}
});
}
function inventreeFormDataUpload(url, data, options={}) {
/* Upload via AJAX using the FormData approach.
*
* Note that the following AJAX parameters are required for FormData upload
*
* processData: false
* contentType: false
*/
// CSRF cookie token
var csrftoken = getCookie('csrftoken');
return $.ajax({
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
method: options.method || 'POST',
data: data,
processData: false,
contentType: false,
success: function(data, status, xhr) {
if (options.success) {
options.success(data, status, xhr);
}
},
error: function(xhr, status, error) {
console.log('Form data upload failure: ' + status);
if (options.error) {
options.error(xhr, status, error);
}
}
});
}
function inventreePut(url, data={}, options={}) {
var method = options.method || 'PUT';
// Middleware token required for data update
//var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var csrftoken = getCookie('csrftoken');
return $.ajax({
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
type: method,
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json',
success: function(response, status) {
if (options.success) {
options.success(response, status);
}
if (options.reloadOnSuccess) {
location.reload();
}
},
error: function(xhr, ajaxOptions, thrownError) {
if (options.error) {
options.error(xhr, ajaxOptions, thrownError);
} else {
console.error(`Error on ${method} to '${url}' - STATUS ${xhr.status}`);
console.error(thrownError);
}
},
complete: function(xhr, status) {
if (options.complete) {
options.complete(xhr, status);
}
}
});
}
function inventreeDelete(url, options={}) {
/*
* Delete a record
*/
options = options || {};
options.method = 'DELETE';
return inventreePut(url, {}, options);
}
function showApiError(xhr) {
var title = null;
var message = null;
switch (xhr.status) {
case 0: // No response
title = '{% trans "No Response" %}';
message = '{% trans "No response from the InvenTree server" %}';
break;
case 400: // Bad request
// Note: Normally error code 400 is handled separately,
// and should now be shown here!
title = '{% trans "Error 400: Bad request" %}';
message = '{% trans "API request returned error code 400" %}';
break;
case 401: // Not authenticated
title = '{% trans "Error 401: Not Authenticated" %}';
message = '{% trans "Authentication credentials not supplied" %}';
break;
case 403: // Permission denied
title = '{% trans "Error 403: Permission Denied" %}';
message = '{% trans "You do not have the required permissions to access this function" %}';
break;
case 404: // Resource not found
title = '{% trans "Error 404: Resource Not Found" %}';
message = '{% trans "The requested resource could not be located on the server" %}';
break;
case 408: // Timeout
title = '{% trans "Error 408: Timeout" %}';
message = '{% trans "Connection timeout while requesting data from server" %}';
break;
default:
title = '{% trans "Unhandled Error Code" %}';
message = `{% trans "Error code" %}: ${xhr.status}`;
break;
}
message += "<hr>";
message += renderErrorMessage(xhr);
showAlertDialog(title, message);
}

View File

@ -0,0 +1,86 @@
{% load i18n %}
function reloadAttachmentTable() {
$('#attachment-table').bootstrapTable("refresh");
}
function loadAttachmentTable(url, options) {
var table = options.table || '#attachment-table';
$(table).inventreeTable({
url: url,
name: options.name || 'attachments',
formatNoMatches: function() { return '{% trans "No attachments found" %}'},
sortable: true,
search: false,
queryParams: options.filters || {},
onPostBody: function() {
// Add callback for 'edit' button
$(table).find('.button-attachment-edit').click(function() {
var pk = $(this).attr('pk');
if (options.onEdit) {
options.onEdit(pk);
}
});
// Add callback for 'delete' button
$(table).find('.button-attachment-delete').click(function() {
var pk = $(this).attr('pk');
if (options.onDelete) {
options.onDelete(pk);
}
});
},
columns: [
{
field: 'attachment',
title: '{% trans "File" %}',
formatter: function(value, row) {
var split = value.split('/');
return renderLink(split[split.length - 1], value);
}
},
{
field: 'comment',
title: '{% trans "Comment" %}',
},
{
field: 'upload_date',
title: '{% trans "Upload Date" %}',
},
{
field: 'actions',
formatter: function(value, row) {
var html = '';
html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton(
'fa-edit icon-blue',
'button-attachment-edit',
row.pk,
'{% trans "Edit attachment" %}',
);
html += makeIconButton(
'fa-trash-alt icon-red',
'button-attachment-delete',
row.pk,
'{% trans "Delete attachment" %}',
);
html += `</div>`;
return html;
}
}
]
});
}

View File

@ -0,0 +1,612 @@
{% load i18n %}
function makeBarcodeInput(placeholderText='', hintText='') {
/*
* Generate HTML for a barcode input
*/
placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}';
hintText = hintText || '{% trans "Enter barcode data" %}';
var html = `
<div class='form-group'>
<label class='control-label' for='barcode'>{% trans "Barcode" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='fas fa-qrcode'></span>
</span>
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
</div>
<div id='hint_barcode_data' class='help-block'>${hintText}</div>
</div>
</div>
`;
return html;
}
function makeNotesField(options={}) {
var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}';
var placeholder = options.placeholder || '{% trans "Enter notes" %}';
return `
<div class='form-group'>
<label class='control-label' for='notes'>{% trans "Notes" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='fas fa-sticky-note'></span>
</span>
<input id='notes' class='textinput textInput form-control' type='text' name='notes' placeholder='${placeholder}'>
</div>
<div id='hint_notes' class='help_block'>${tooltip}</div>
</div>
</div>`;
}
/*
* POST data to the server, and handle standard responses.
*/
function postBarcodeData(barcode_data, options={}) {
var modal = options.modal || '#modal-form';
var url = options.url || '/api/barcode/';
var data = options.data || {};
data.barcode = barcode_data;
inventreePut(
url,
data,
{
method: 'POST',
error: function() {
enableBarcodeInput(modal, true);
showBarcodeMessage(modal, '{% trans "Server error" %}');
},
success: function(response, status) {
modalEnable(modal, false);
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
if (options.onScan) {
options.onScan(response);
}
} else if ('error' in response) {
showBarcodeMessage(
modal,
response.error,
'warning'
);
} else {
showBarcodeMessage(
modal,
'{% trans "Unknown response from server" %}',
'warning'
);
}
} else {
// Invalid response returned from server
showInvalidResponseError(modal, response, status);
}
}
}
)
}
function showBarcodeMessage(modal, message, style='danger') {
var html = `<div class='alert alert-block alert-${style}'>`;
html += message;
html += "</div>";
$(modal + ' #barcode-error-message').html(html);
}
function showInvalidResponseError(modal, response, status) {
showBarcodeMessage(modal, `{% trans "Invalid server response" %}<br>{% trans "Status" %}: '${status}'`);
}
function enableBarcodeInput(modal, enabled=true) {
var barcode = $(modal + ' #barcode');
barcode.prop('disabled', !enabled);
modalEnable(modal, enabled);
barcode.focus();
}
function getBarcodeData(modal) {
modal = modal || '#modal-form';
var el = $(modal + ' #barcode');
var barcode = el.val();
el.val('');
el.focus();
return barcode.trim();
}
function barcodeDialog(title, options={}) {
/*
* Handle a barcode display dialog.
*/
var modal = '#modal-form';
function sendBarcode() {
var barcode = getBarcodeData(modal);
if (barcode && barcode.length > 0) {
postBarcodeData(barcode, options);
}
}
$(modal).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0);
var barcode = $(modal + ' #barcode');
// Handle 'enter' key on barcode
barcode.keyup(function(event) {
event.preventDefault();
if (event.which == 10 || event.which == 13) {
sendBarcode();
}
});
// Ensure the barcode field has focus
barcode.focus();
var form = $(modal).find('.js-modal-form');
// Override form submission
form.submit(function() {
return false;
});
// Callback for when the "submit" button is pressed on the modal
modalSubmit(modal, function() {
if (options.onSubmit) {
options.onSubmit();
}
});
if (options.onShow) {
options.onShow();
}
});
modalSetTitle(modal, title);
if (options.onSubmit) {
modalShowSubmitButton(modal, true);
} else {
modalShowSubmitButton(modal, false);
}
var content = '';
content += `<div class='alert alert-info alert-block'>{% trans "Scan barcode data below" %}</div>`;
content += `<div id='barcode-error-message'></div>`;
content += `<form class='js-modal-form' method='post'>`;
// Optional content before barcode input
content += `<div class='container' id='barcode-header'>`;
content += options.headerContent || '';
content += `</div>`;
content += makeBarcodeInput();
if (options.extraFields) {
content += options.extraFields;
}
content += `</form>`;
// Optional content after barcode input
content += `<div class='container' id='barcode-footer'>`;
content += options.footerContent || '';
content += '</div>';
modalSetContent(modal, content);
$(modal).modal({
backdrop: 'static',
keyboard: false,
});
if (options.preShow) {
options.preShow();
}
$(modal).modal('show');
}
function barcodeScanDialog() {
/*
* Perform a barcode scan,
* and (potentially) redirect the browser
*/
var modal = '#modal-form';
barcodeDialog(
"Scan Barcode",
{
onScan: function(response) {
if ('url' in response) {
$(modal).modal('hide');
// Redirect to the URL!
window.location.href = response.url;
} else {
showBarcodeMessage(
modal,
'{% trans "No URL in response" %}',
'warning'
);
}
}
},
);
}
/*
* Dialog for linking a particular barcode to a stock item.
*/
function linkBarcodeDialog(stockitem, options={}) {
var modal = '#modal-form';
barcodeDialog(
"{% trans 'Link Barcode to Stock Item' %}",
{
url: '/api/barcode/link/',
data: {
stockitem: stockitem,
},
onScan: function(response) {
$(modal).modal('hide');
location.reload();
}
}
);
}
/*
* Remove barcode association from a device.
*/
function unlinkBarcode(stockitem) {
var html = `<b>{% trans "Unlink Barcode" %}</b><br>`;
html += "{% trans 'This will remove the association between this stock item and the barcode' %}";
showQuestionDialog(
"{% trans 'Unlink Barcode' %}",
html,
{
accept_text: "{% trans 'Unlink' %}",
accept: function() {
inventreePut(
`/api/stock/${stockitem}/`,
{
// Clear the UID field
uid: '',
},
{
method: 'PATCH',
success: function(response, status) {
location.reload();
},
},
);
},
}
);
}
/*
* Display dialog to check multiple stock items in to a stock location.
*/
function barcodeCheckIn(location_id, options={}) {
var modal = '#modal-form';
// List of items we are going to checkin
var items = [];
function reloadTable() {
modalEnable(modal, false);
// Remove click listeners
$(modal + ' .button-item-remove').unbind('click');
var table = $(modal + ' #items-table-div');
var html = `
<table class='table table-condensed table-striped' id='items-table'>
<thead>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Location" %}</th>
<th>{% trans "Quantity" %}</th>
<th></th>
</tr>
</thead>
<tbody>`;
items.forEach(function(item) {
html += `
<tr pk='${item.pk}'>
<td>${imageHoverIcon(item.part_detail.thumbnail)} ${item.part_detail.name}</td>
<td>${item.location_detail.name}</td>
<td>${item.quantity}</td>
<td>${makeIconButton('fa-times-circle icon-red', 'button-item-remove', item.pk, '{% trans "Remove stock item" %}')}</td>
</tr>`;
});
html += `
</tbody>
</table>`;
table.html(html);
modalEnable(modal, items.length > 0);
$(modal + ' #barcode').focus();
$(modal + ' .button-item-remove').unbind('click').on('mouseup', function() {
var pk = $(this).attr('pk');
var match = false;
for (var ii = 0; ii < items.length; ii++) {
if (pk.toString() == items[ii].pk.toString()) {
items.splice(ii, 1);
match = true;
break;
}
}
if (match) {
reloadTable();
}
return false;
});
}
var table = `<div class='container' id='items-table-div' style='width: 80%; float: left;'></div>`;
// Extra form fields
var extra = makeNotesField();
barcodeDialog(
'{% trans "Check Stock Items into Location" %}',
{
headerContent: table,
preShow: function() {
modalSetSubmitText(modal, '{% trans "Check In" %}');
modalEnable(modal, false);
reloadTable();
},
onShow: function() {
},
extraFields: extra,
onSubmit: function() {
// Called when the 'check-in' button is pressed
var data = {location: location_id};
// Extract 'notes' field
data.notes = $(modal + ' #notes').val();
var entries = [];
items.forEach(function(item) {
entries.push({
pk: item.pk,
quantity: item.quantity,
});
});
data.items = entries;
inventreePut(
"{% url 'api-stock-transfer' %}",
data,
{
method: 'POST',
success: function(response, status) {
// Hide the modal
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
showAlertOrCache('alert-success', response.success, true);
location.reload();
} else {
showAlertOrCache('alert-success', '{% trans "Error transferring stock" %}', false);
}
}
}
);
},
onScan: function(response) {
if ('stockitem' in response) {
stockitem = response.stockitem;
var duplicate = false;
items.forEach(function(item) {
if (item.pk == stockitem.pk) {
duplicate = true;
}
});
if (duplicate) {
showBarcodeMessage(modal, '{% trans "Stock Item already scanned" %}', "warning");
} else {
if (stockitem.location == location_id) {
showBarcodeMessage(modal, '{% trans "Stock Item already in this location" %}');
return;
}
// Add this stock item to the list
items.push(stockitem);
showBarcodeMessage(modal, '{% trans "Added stock item" %}', "success");
reloadTable();
}
} else {
// Barcode does not match a stock item
showBarcodeMessage(modal, '{% trans "Barcode does not match Stock Item" %}', "warning");
}
},
}
);
}
/*
* Display dialog to check a single stock item into a stock location
*/
function scanItemsIntoLocation(item_id_list, options={}) {
var modal = options.modal || '#modal-form';
var stock_location = null;
// Extra form fields
var extra = makeNotesField();
// Header contentfor
var header = `
<div id='header-div'>
</div>
`;
function updateLocationInfo(location) {
var div = $(modal + ' #header-div');
if (stock_location && stock_location.pk) {
div.html(`
<div class='alert alert-block alert-info'>
<b>{% trans "Location" %}</b></br>
${stock_location.name}<br>
<i>${stock_location.description}</i>
</div>
`);
} else {
div.html('');
}
}
barcodeDialog(
'{% trans "Check Into Location" %}',
{
headerContent: header,
extraFields: extra,
preShow: function() {
modalSetSubmitText(modal, '{% trans "Check In" %}');
modalEnable(modal, false);
},
onShow: function() {
},
onSubmit: function() {
// Called when the 'check-in' button is pressed
if (!stock_location) {
return;
}
var items = [];
item_id_list.forEach(function(pk) {
items.push({
pk: pk,
});
})
var data = {
location: stock_location.pk,
notes: $(modal + ' #notes').val(),
items: items,
};
// Send API request
inventreePut(
'{% url "api-stock-transfer" %}',
data,
{
method: 'POST',
success: function(response, status) {
// First hide the modal
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
showAlertOrCache('alert-success', response.success, true);
location.reload();
} else {
showAlertOrCache('alert-danger', '{% trans "Error transferring stock" %}', false);
}
}
}
)
},
onScan: function(response) {
updateLocationInfo(null);
if ('stocklocation' in response) {
// Barcode corresponds to a StockLocation
stock_location = response.stocklocation;
updateLocationInfo(stock_location);
modalEnable(modal, true);
} else {
// Barcode does *NOT* correspond to a StockLocation
showBarcodeMessage(
modal,
'{% trans "Barcode does not match a valid location" %}',
"warning",
);
}
}
}
)
}

View File

@ -0,0 +1,560 @@
{% load i18n %}
/* BOM management functions.
* Requires follwing files to be loaded first:
* - api.js
* - part.js
* - modals.js
*/
function reloadBomTable(table, options) {
table.bootstrapTable('refresh');
}
function removeRowFromBomWizard(e) {
/* Remove a row from BOM upload wizard
*/
e = e || window.event;
var src = e.target || e.srcElement;
var table = $(src).closest('table');
// Which column was clicked?
var row = $(src).closest('tr');
row.remove();
var rowNum = 1;
var colNum = 0;
table.find('tr').each(function() {
colNum++;
if (colNum >= 3) {
var cell = $(this).find('td:eq(1)');
cell.text(rowNum++);
}
});
}
function removeColFromBomWizard(e) {
/* Remove a column from BOM upload wizard
*/
e = e || window.event;
var src = e.target || e.srcElement;
// Which column was clicked?
var col = $(src).closest('th').index();
var table = $(src).closest('table');
table.find('tr').each(function() {
this.removeChild(this.cells[col]);
});
}
function newPartFromBomWizard(e) {
/* Create a new part directly from the BOM wizard.
*/
e = e || window.event;
var src = e.target || e.srcElement;
var row = $(src).closest('tr');
launchModalForm('/part/new/', {
data: {
'description': row.attr('part-description'),
'name': row.attr('part-name'),
},
success: function(response) {
/* A new part has been created! Push it as an option.
*/
var select = row.attr('part-select');
var option = new Option(response.text, response.pk, true, true);
$(select).append(option).trigger('change');
}
});
}
function loadBomTable(table, options) {
/* Load a BOM table with some configurable options.
*
* Following options are available:
* editable - Should the BOM table be editable?
* bom_url - Address to request BOM data from
* part_url - Address to request Part data from
* parent_id - Parent ID of the owning part
*
* BOM data are retrieved from the server via AJAX query
*/
var params = {
part: options.parent_id,
ordering: 'name',
}
if (options.part_detail) {
params.part_detail = true;
}
params.sub_part_detail = true;
var filters = {};
if (!options.disableFilters) {
filters = loadTableFilters('bom');
}
for (var key in params) {
filters[key] = params[key];
}
setupFilterList('bom', $(table));
// Construct the table columns
var cols = [];
if (options.editable) {
cols.push({
field: 'ID',
title: '',
checkbox: true,
visible: true,
switchable: false,
formatter: function(value, row, index, field) {
// Disable checkbox if the row is defined for a *different* part!
if (row.part != options.parent_id) {
return {
disabled: true,
};
} else {
return value;
}
}
});
}
// Set the parent ID of the multi-level table.
// We prepend this with the literal string value 'top-level-',
// because otherwise the unfortunate situation where BomItem.pk == BomItem.part.pk
// AND THIS BREAKS EVERYTHING
var parent_id = `top-level-${options.parent_id}`;
// Part column
cols.push(
{
field: 'sub_part',
title: '{% trans "Part" %}',
sortable: true,
formatter: function(value, row, index, field) {
var url = `/part/${row.sub_part}/`;
var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
var sub_part = row.sub_part_detail;
html += makePartIcons(row.sub_part_detail);
// Display an extra icon if this part is an assembly
if (sub_part.assembly) {
var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream label-right'></span>`;
html += renderLink(text, `/part/${row.sub_part}/bom/`);
}
return html;
}
}
);
// Part description
cols.push(
{
field: 'sub_part_detail.description',
title: '{% trans "Description" %}',
}
);
// Part reference
cols.push({
field: 'reference',
title: '{% trans "Reference" %}',
searchable: true,
sortable: true,
});
// Part quantity
cols.push({
field: 'quantity',
title: '{% trans "Quantity" %}',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
var text = value;
// The 'value' is a text string with (potentially) multiple trailing zeros
// Let's make it a bit more pretty
text = parseFloat(text);
if (row.optional) {
text += ' ({% trans "Optional" %})';
}
if (row.overage) {
text += `<small> (${row.overage}) </small>`;
}
return text;
},
});
cols.push(
{
field: 'sub_part_detail.stock',
title: '{% trans "Available" %}',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
var url = `/part/${row.sub_part_detail.pk}/stock/`;
var text = value;
if (value == null || value <= 0) {
text = `<span class='label label-warning'>{% trans "No Stock" %}</span>`;
}
return renderLink(text, url);
}
});
cols.push(
{
field: 'purchase_price_range',
title: '{% trans "Purchase Price Range" %}',
searchable: false,
sortable: true,
});
cols.push(
{
field: 'purchase_price_avg',
title: '{% trans "Purchase Price Average" %}',
searchable: false,
sortable: true,
});
cols.push(
{
field: 'price_range',
title: '{% trans "Buy Price" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
return "<span class='warning-msg'>{% trans 'No pricing available' %}</span>";
}
}
});
cols.push({
field: 'optional',
title: '{% trans "Optional" %}',
searchable: false,
formatter: function(value) {
return yesNoLabel(value);
}
});
cols.push({
field: 'allow_variants',
title: '{% trans "Allow Variants" %}',
formatter: function(value) {
return yesNoLabel(value);
}
})
cols.push({
field: 'inherited',
title: '{% trans "Inherited" %}',
searchable: false,
formatter: function(value, row, index, field) {
// This BOM item *is* inheritable, but is defined for this BOM
if (!row.inherited) {
return yesNoLabel(false);
} else if (row.part == options.parent_id) {
return '{% trans "Inherited" %}';
} else {
// If this BOM item is inherited from a parent part
return renderLink(
'{% trans "View BOM" %}',
`/part/${row.part}/bom/`,
);
}
}
});
cols.push(
{
'field': 'can_build',
'title': '{% trans "Can Build" %}',
formatter: function(value, row, index, field) {
var can_build = 0;
if (row.quantity > 0) {
can_build = row.sub_part_detail.stock / row.quantity;
}
return +can_build.toFixed(2);
},
sorter: function(valA, valB, rowA, rowB) {
// Function to sort the "can build" quantity
var cb_a = 0;
var cb_b = 0;
if (rowA.quantity > 0) {
cb_a = rowA.sub_part_detail.stock / rowA.quantity;
}
if (rowB.quantity > 0) {
cb_b = rowB.sub_part_detail.stock / rowB.quantity;
}
return (cb_a > cb_b) ? 1 : -1;
},
sortable: true,
}
)
// Part notes
cols.push(
{
field: 'note',
title: '{% trans "Notes" %}',
searchable: true,
sortable: true,
}
);
if (options.editable) {
cols.push({
title: '{% trans "Actions" %}',
switchable: false,
field: 'pk',
visible: true,
formatter: function(value, row, index, field) {
if (row.part == options.parent_id) {
var bValidate = `<button title='{% trans "Validate BOM Item" %}' class='bom-validate-button btn btn-default btn-glyph' type='button' pk='${row.pk}'><span class='fas fa-check-circle icon-blue'/></button>`;
var bValid = `<span title='{% trans "This line has been validated" %}' class='fas fa-check-double icon-green'/>`;
var bEdit = `<button title='{% trans "Edit BOM Item" %}' class='bom-edit-button btn btn-default btn-glyph' type='button' pk='${row.pk}'><span class='fas fa-edit'></span></button>`;
var bDelt = `<button title='{% trans "Delete BOM Item" %}' class='bom-delete-button btn btn-default btn-glyph' type='button' pk='${row.pk}'><span class='fas fa-trash-alt icon-red'></span></button>`;
var html = "<div class='btn-group' role='group'>";
html += bEdit;
html += bDelt;
if (!row.validated) {
html += bValidate;
} else {
html += bValid;
}
html += "</div>";
return html;
} else {
// Return a link to the external BOM
return renderLink(
'{% trans "View BOM" %}',
`/part/${row.part}/bom/`
);
}
}
});
}
// Function to request BOM data for sub-items
// This function may be called recursively for multi-level BOMs
function requestSubItems(bom_pk, part_pk) {
inventreeGet(
options.bom_url,
{
part: part_pk,
sub_part_detail: true,
},
{
success: function(response) {
for (var idx = 0; idx < response.length; idx++) {
response[idx].parentId = bom_pk;
if (response[idx].sub_part_detail.assembly) {
requestSubItems(response[idx].pk, response[idx].sub_part)
}
}
table.bootstrapTable('append', response);
table.treegrid('collapseAll');
},
error: function() {
console.log('Error requesting BOM for part=' + part_pk);
}
}
)
}
table.inventreeTable({
treeEnable: !options.editable,
rootParentId: parent_id,
idField: 'pk',
parentIdField: 'parentId',
treeShowField: 'sub_part',
showColumns: true,
name: 'bom',
sortable: true,
search: true,
rowStyle: function(row, index) {
var classes = [];
// Shade rows differently if they are for different parent parts
if (row.part != options.parent_id) {
classes.push('rowinherited');
}
if (row.validated) {
classes.push('rowvalid');
} else {
classes.push('rowinvalid');
}
return {
classes: classes.join(' '),
};
},
formatNoMatches: function() {
return '{% trans "No BOM items found" %}';
},
clickToSelect: true,
queryParams: filters,
original: params,
columns: cols,
url: options.bom_url,
onPostBody: function() {
if (!options.editable) {
table.treegrid({
treeColumn: 0,
onExpand: function() {
}
});
}
},
onLoadSuccess: function() {
if (options.editable) {
table.bootstrapTable('uncheckAll');
} else {
var data = table.bootstrapTable('getData');
for (var idx = 0; idx < data.length; idx++) {
var row = data[idx];
// If a row already has a parent ID set, it's already been updated!
if (row.parentId) {
continue;
}
// Set the parent ID of the top-level rows
row.parentId = parent_id;
table.bootstrapTable('updateRow', idx, row, true);
if (row.sub_part_detail.assembly) {
requestSubItems(row.pk, row.sub_part);
}
}
}
},
});
// In editing mode, attached editables to the appropriate table elements
if (options.editable) {
table.on('click', '.bom-delete-button', function() {
var pk = $(this).attr('pk');
var url = `/part/bom/${pk}/delete/`;
constructForm(`/api/bom/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete BOM Item" %}',
onSuccess: function() {
reloadBomTable(table);
}
});
});
table.on('click', '.bom-edit-button', function() {
var pk = $(this).attr('pk');
var url = `/part/bom/${pk}/edit/`;
launchModalForm(
url,
{
success: function() {
reloadBomTable(table);
}
}
);
});
table.on('click', '.bom-validate-button', function() {
var pk = $(this).attr('pk');
var url = `/api/bom/${pk}/validate/`;
inventreePut(
url,
{
valid: true
},
{
method: 'PATCH',
success: function() {
reloadBomTable(table);
}
}
);
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{% load i18n %}
/**
* Helper functions for calendar display
*/
function startDate(calendar) {
// Extract the first displayed date on the calendar
return calendar.currentData.dateProfile.activeRange.start.toISOString().split("T")[0];
}
function endDate(calendar) {
// Extract the last display date on the calendar
return calendar.currentData.dateProfile.activeRange.end.toISOString().split("T")[0];
}
function clearEvents(calendar) {
// Remove all events from the calendar
var events = calendar.getEvents();
events.forEach(function(event) {
event.remove();
})
}

View File

@ -0,0 +1,800 @@
{% load i18n %}
function manufacturerPartFields() {
return {
part: {},
manufacturer: {},
MPN: {
icon: 'fa-hashtag',
},
description: {},
link: {
icon: 'fa-link',
}
};
}
function createManufacturerPart(options={}) {
var fields = manufacturerPartFields();
if (options.part) {
fields.part.value = options.part;
fields.part.hidden = true;
}
if (options.manufacturer) {
fields.manufacturer.value = options.manufacturer;
}
constructForm('{% url "api-manufacturer-part-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Manufacturer Part" %}',
onSuccess: options.onSuccess
});
}
function editManufacturerPart(part, options={}) {
var url = `/api/company/part/manufacturer/${part}/`;
constructForm(url, {
fields: manufacturerPartFields(),
title: '{% trans "Edit Manufacturer Part" %}',
onSuccess: options.onSuccess
});
}
function deleteManufacturerPart(part, options={}) {
constructForm(`/api/company/part/manufacturer/${part}/`, {
method: 'DELETE',
title: '{% trans "Delete Manufacturer Part" %}',
onSuccess: options.onSuccess,
});
}
function supplierPartFields() {
return {
part: {},
supplier: {},
SKU: {
icon: 'fa-hashtag',
},
manufacturer_part: {
filters: {
part_detail: true,
manufacturer_detail: true,
}
},
description: {},
link: {
icon: 'fa-link',
},
note: {
icon: 'fa-pencil-alt',
},
packaging: {
icon: 'fa-box',
}
};
}
/*
* Launch a form to create a new ManufacturerPart
*/
function createSupplierPart(options={}) {
var fields = supplierPartFields();
if (options.part) {
fields.manufacturer_part.filters.part = options.part;
fields.part.hidden = true;
fields.part.value = options.part;
}
if (options.supplier) {
fields.supplier.value = options.supplier;
}
if (options.manufacturer_part) {
fields.manufacturer_part.value = options.manufacturer_part;
}
constructForm('{% url "api-supplier-part-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Supplier Part" %}',
onSuccess: options.onSuccess,
});
}
function editSupplierPart(part, options={}) {
constructForm(`/api/company/part/${part}/`, {
fields: supplierPartFields(),
title: '{% trans "Edit Supplier Part" %}',
onSuccess: options.onSuccess
});
}
function deleteSupplierPart(part, options={}) {
constructForm(`/api/company/part/${part}/`, {
method: 'DELETE',
title: '{% trans "Delete Supplier Part" %}',
onSuccess: options.onSuccess,
});
}
// Returns a default form-set for creating / editing a Company object
function companyFormFields(options={}) {
return {
name: {},
description: {},
website: {
icon: 'fa-globe',
},
address: {
icon: 'fa-envelope',
},
currency: {
icon: 'fa-dollar-sign',
},
phone: {
icon: 'fa-phone',
},
email: {
icon: 'fa-at',
},
contact: {
icon: 'fa-address-card',
},
is_supplier: {},
is_manufacturer: {},
is_customer: {}
};
}
function editCompany(pk, options={}) {
var fields = options.fields || companyFormFields();
constructForm(
`/api/company/${pk}/`,
{
method: 'PATCH',
fields: fields,
reload: true,
title: '{% trans "Edit Company" %}',
}
);
};
/*
* Launches a form to create a new company.
* As this can be called from many different contexts,
* we abstract it here!
*/
function createCompany(options={}) {
// Default field set
var fields = options.fields || companyFormFields();
constructForm(
'{% url "api-company-list" %}',
{
method: 'POST',
fields: fields,
follow: true,
title: '{% trans "Add new Company" %}',
}
);
}
function loadCompanyTable(table, url, options={}) {
/*
* Load company listing data into specified table.
*
* Args:
* - table: Table element on the page
* - url: Base URL for the API query
* - options: table options.
*/
// Query parameters
var params = options.params || {};
var filters = loadTableFilters("company");
for (var key in params) {
filters[key] = params[key];
}
setupFilterList("company", $(table));
var columns = [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'name',
title: '{% trans "Company" %}',
sortable: true,
switchable: false,
formatter: function(value, row, index, field) {
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>`;
}
if (row.is_manufacturer) {
html += `<span title='{% trans "Manufacturer" %}' class='fas fa-industry label-right'></span>`;
}
if (row.is_supplier) {
html += `<span title='{% trans "Supplier" %}' class='fas fa-building label-right'></span>`;
}
return html;
}
},
{
field: 'description',
title: '{% trans "Description" %}',
},
{
field: 'website',
title: '{% trans "Website" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
}
return '';
}
},
];
if (options.pagetype == 'suppliers') {
columns.push({
sortable: true,
field: 'parts_supplied',
title: '{% trans "Parts Supplied" %}',
formatter: function(value, row) {
return renderLink(value, `/company/${row.pk}/parts/`);
}
});
} else if (options.pagetype == 'manufacturers') {
columns.push({
sortable: true,
field: 'parts_manufactured',
title: '{% trans "Parts Manufactured" %}',
formatter: function(value, row) {
return renderLink(value, `/company/${row.pk}/parts/`);
}
});
}
$(table).inventreeTable({
url: url,
method: 'get',
queryParams: filters,
groupBy: false,
sidePagination: 'server',
formatNoMatches: function() { return "{% trans "No company information found" %}"; },
showColumns: true,
name: options.pagetype || 'company',
columns: columns,
});
}
function deleteManufacturerParts(selections, options={}) {
if (selections.length == 0) {
return;
}
var parts = [];
var text = `
<div class='alert alert-block alert-danger'>
<p>{% trans "The following manufacturer parts will be deleted" %}:</p>
<ul>`;
selections.forEach(function(item) {
parts.push(item.pk);
text += `
<li>
<p>${item.MPN} - ${item.part_detail.full_name}</p>
</li>`;
});
text += `
</ul>
</div>`;
showQuestionDialog(
'{% trans "Delete Manufacturer Parts" %}',
text,
{
accept_text: '{% trans "Delete" %}',
accept: function() {
// Delete each manufacturer part
var requests = [];
parts.forEach(function(pk) {
var url = `/api/company/part/manufacturer/${pk}`;
requests.push(inventreeDelete(url));
});
// Wait for all the requests to complete
$.when.apply($, requests).done(function() {
if (options.onSuccess) {
options.onSuccess();
}
})
}
}
);
}
function loadManufacturerPartTable(table, url, options) {
/*
* Load manufacturer part table
*
*/
// Query parameters
var params = options.params || {};
// Load filters
var filters = loadTableFilters("manufacturer-part");
for (var key in params) {
filters[key] = params[key];
}
setupFilterList("manufacturer-part", $(table));
$(table).inventreeTable({
url: url,
method: 'get',
original: params,
queryParams: filters,
name: 'manufacturerparts',
groupBy: false,
formatNoMatches: function() { return '{% trans "No manufacturer parts found" %}'; },
columns: [
{
checkbox: true,
switchable: false,
},
{
visible: params['part_detail'],
switchable: params['part_detail'],
sortable: true,
field: 'part_detail.full_name',
title: '{% trans "Part" %}',
formatter: function(value, row, index, field) {
var url = `/part/${row.part}/`;
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>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
}
return html;
}
},
{
sortable: true,
field: 'manufacturer',
title: '{% trans "Manufacturer" %}',
formatter: function(value, row, index, field) {
if (value && row.manufacturer_detail) {
var name = row.manufacturer_detail.name;
var url = `/company/${value}/`;
var html = imageHoverIcon(row.manufacturer_detail.image) + renderLink(name, url);
return html;
} else {
return "-";
}
}
},
{
sortable: true,
field: 'MPN',
title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
return renderLink(value, `/manufacturer-part/${row.pk}/`);
}
},
{
field: 'link',
title: '{% trans "Link" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
} else {
return '';
}
}
},
{
field: 'description',
title: '{% trans "Description" %}',
sortable: false,
switchable: true,
},
{
field: 'actions',
title: '',
sortable: false,
switchable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-manufacturer-part-edit', pk, '{% trans "Edit manufacturer part" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-manufacturer-part-delete', pk, '{% trans "Delete manufacturer part" %}');
html += '</div>';
return html;
}
}
],
onPostBody: function() {
// Callbacks
$(table).find('.button-manufacturer-part-edit').click(function() {
var pk = $(this).attr('pk');
editManufacturerPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-manufacturer-part-delete').click(function() {
var pk = $(this).attr('pk');
deleteManufacturerPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
})
}
});
}
function loadManufacturerPartParameterTable(table, url, options) {
/*
* Load table of ManufacturerPartParameter objects
*/
var params = options.params || {};
// Load filters
var filters = loadTableFilters("manufacturer-part-parameters");
// Overwrite explicit parameters
for (var key in params) {
filters[key] = params[key];
}
// setupFilterList("manufacturer-part-parameters", $(table));
$(table).inventreeTable({
url: url,
method: 'get',
original: params,
queryParams: filters,
name: 'manufacturerpartparameters',
groupBy: false,
formatNoMatches: function() { return '{% trans "No parameters found" %}'; },
columns: [
{
checkbox: true,
switchable: false,
visible: true,
},
{
field: 'name',
title: '{% trans "Name" %}',
switchable: false,
sortable: true,
},
{
field: 'value',
title: '{% trans "Value" %}',
switchable: false,
sortable: true,
},
{
field: 'units',
title: '{% trans "Units" %}',
switchable: true,
sortable: true,
},
{
field: 'actions',
title: '',
switchable: false,
sortable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}');
html += `</div>`;
return html;
}
}
],
onPostBody: function() {
// Setup callback functions
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, {
fields: {
name: {},
value: {},
units: {},
},
title: '{% trans "Edit Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-parameter-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
}
});
}
function loadSupplierPartTable(table, url, options) {
/*
* Load supplier part table
*
*/
// Query parameters
var params = options.params || {};
// Load filters
var filters = loadTableFilters("supplier-part");
for (var key in params) {
filters[key] = params[key];
}
setupFilterList("supplier-part", $(table));
$(table).inventreeTable({
url: url,
method: 'get',
original: params,
queryParams: filters,
name: 'supplierparts',
groupBy: false,
formatNoMatches: function() { return '{% trans "No supplier parts found" %}'; },
columns: [
{
checkbox: true,
switchable: false,
},
{
visible: params['part_detail'],
switchable: params['part_detail'],
sortable: true,
field: 'part_detail.full_name',
title: '{% trans "Part" %}',
formatter: function(value, row, index, field) {
var url = `/part/${row.part}/`;
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>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
}
return html;
}
},
{
sortable: true,
field: 'supplier',
title: '{% trans "Supplier" %}',
formatter: function(value, row, index, field) {
if (value) {
var name = row.supplier_detail.name;
var url = `/company/${value}/`;
var html = imageHoverIcon(row.supplier_detail.image) + renderLink(name, url);
return html;
} else {
return "-";
}
},
},
{
sortable: true,
field: 'SKU',
title: '{% trans "Supplier Part" %}',
formatter: function(value, row, index, field) {
return renderLink(value, `/supplier-part/${row.pk}/`);
}
},
{
visible: params['manufacturer_detail'],
switchable: params['manufacturer_detail'],
sortable: true,
field: 'manufacturer',
title: '{% trans "Manufacturer" %}',
formatter: function(value, row, index, field) {
if (value && row.manufacturer_detail) {
var name = row.manufacturer_detail.name;
var url = `/company/${value}/`;
var html = imageHoverIcon(row.manufacturer_detail.image) + renderLink(name, url);
return html;
} else {
return "-";
}
}
},
{
visible: params['manufacturer_detail'],
switchable: params['manufacturer_detail'],
sortable: true,
field: 'MPN',
title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
if (value && row.manufacturer_part) {
return renderLink(value, `/manufacturer-part/${row.manufacturer_part}/`);
} else {
return "-";
}
}
},
{
field: 'link',
title: '{% trans "Link" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
} else {
return '';
}
}
},
{
field: 'description',
title: '{% trans "Description" %}',
sortable: false,
},
{
field: 'note',
title: '{% trans "Notes" %}',
sortable: false,
},
{
field: 'packaging',
title: '{% trans "Packaging" %}',
sortable: false,
},
{
field: 'actions',
title: '',
sortable: false,
switchable: false,
formatter: function(value, row) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-supplier-part-edit', pk, '{% trans "Edit supplier part" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-supplier-part-delete', pk, '{% trans "Delete supplier part" %}');
html += '</div>';
return html;
}
}
],
onPostBody: function() {
// Callbacks
$(table).find('.button-supplier-part-edit').click(function() {
var pk = $(this).attr('pk');
editSupplierPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-supplier-part-delete').click(function() {
var pk = $(this).attr('pk');
deleteSupplierPart(
pk,
{
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
})
}
});
}

View File

@ -0,0 +1,419 @@
{% load i18n %}
/**
* Code for managing query filters / table options.
*
* Optional query filters are available to the user for various
* tables display in the web interface.
* These filters are saved to the web session, and should be
* persistent for a given table type.
*
* This makes use of the 'inventreeSave' and 'inventreeLoad' functions
* for writing to and reading from session storage.
*
*/
function defaultFilters() {
return {
stock: "cascade=1&in_stock=1",
build: "",
parts: "cascade=1",
company: "",
salesorder: "",
purchaseorder: "",
};
}
/**
* Load table filters for the given table from session storage
*
* @param tableKey - String key for the particular table
* @param defaults - Default filters for this table e.g. 'cascade=1&location=5'
*/
function loadTableFilters(tableKey) {
var lookup = "table-filters-" + tableKey.toLowerCase();
var defaults = defaultFilters()[tableKey] || '';
var filterstring = inventreeLoad(lookup, defaults);
var filters = {};
filterstring.split("&").forEach(function(item, index) {
item = item.trim();
if (item.length > 0) {
var f = item.split('=');
if (f.length == 2) {
filters[f[0]] = f[1];
} else {
console.log(`Improperly formatted filter: ${item}`);
}
}
});
return filters;
}
/**
* Save table filters to session storage
*
* @param {*} tableKey - string key for the given table
* @param {*} filters - object of string:string pairs
*/
function saveTableFilters(tableKey, filters) {
var lookup = "table-filters-" + tableKey.toLowerCase();
var strings = [];
for (var key in filters) {
strings.push(`${key.trim()}=${String(filters[key]).trim()}`);
}
var filterstring = strings.join('&');
inventreeSave(lookup, filterstring);
}
/*
* Remove a named filter parameter
*/
function removeTableFilter(tableKey, filterKey) {
var filters = loadTableFilters(tableKey);
delete filters[filterKey];
saveTableFilters(tableKey, filters);
// Return a copy of the updated filters
return filters;
}
function addTableFilter(tableKey, filterKey, filterValue) {
var filters = loadTableFilters(tableKey);
filters[filterKey] = filterValue;
saveTableFilters(tableKey, filters);
// Return a copy of the updated filters
return filters;
}
/*
* Clear all the custom filters for a given table
*/
function clearTableFilters(tableKey) {
saveTableFilters(tableKey, {});
return {};
}
/*
* Return a list of the "available" filters for a given table key.
* A filter is "available" if it is not already being used to filter the table.
* Once a filter is selected, it will not be returned here.
*/
function getRemainingTableFilters(tableKey) {
var filters = loadTableFilters(tableKey);
var remaining = getAvailableTableFilters(tableKey);
for (var key in filters) {
// Delete the filter if it is already in use
delete remaining[key];
}
return remaining;
}
/*
* Return the filter settings for a given table and key combination.
* Return empty object if the combination does not exist.
*/
function getFilterSettings(tableKey, filterKey) {
return getAvailableTableFilters(tableKey)[filterKey] || {};
}
/*
* Return a set of key:value options for the given filter.
* If no options are specified (e.g. for a number field),
* then a null object is returned.
*/
function getFilterOptionList(tableKey, filterKey) {
var settings = getFilterSettings(tableKey, filterKey);
if (settings.type == 'bool') {
return {
'1': {
key: '1',
value: '{% trans "true" %}',
},
'0': {
key: '0',
value: '{% trans "false" %}',
},
};
} else if ('options' in settings) {
return settings.options;
}
return null;
}
/*
* Generate a list of <option> tags for the given table.
*/
function generateAvailableFilterList(tableKey) {
var remaining = getRemainingTableFilters(tableKey);
var id = 'filter-tag-' + tableKey.toLowerCase();
var html = `<select class='form-control filter-input' id='${id}' name='tag'>`;
html += "<option value=''>{% trans 'Select filter' %}</option>";
for (var opt in remaining) {
var title = getFilterTitle(tableKey, opt);
html += `<option value='${opt}'>${title}</option>`;
}
html += `</select>`;
return html;
}
/*
* Generate an input for setting the value of a given filter.
*/
function generateFilterInput(tableKey, filterKey) {
var id = 'filter-value-' + tableKey.toLowerCase();
if (filterKey == null || filterKey.length == 0) {
// Return an 'empty' element
return `<div class='filter-input' id='${id}'></div>`;
}
var options = getFilterOptionList(tableKey, filterKey);
var html = '';
// A 'null' options list means that a simple text-input dialog should be used
if (options == null) {
html = `<input class='form-control filter-input' id='${id}' name='value'></input>`;
} else {
// Return a 'select' input with the available values
html = `<select class='form-control filter-input' id='${id}' name='value'>`;
for (var key in options) {
option = options[key];
html += `<option value='${key}'>${option.value}</option>`;
}
html += `</select>`;
}
return html;
}
/**
* Configure a filter list for a given table
*
* @param {*} tableKey - string lookup key for filter settings
* @param {*} table - bootstrapTable element to update
* @param {*} target - name of target element on page
*/
function setupFilterList(tableKey, table, target) {
var addClicked = false;
if (target == null || target.length == 0) {
target = `#filter-list-${tableKey}`;
}
var tag = `filter-tag-${tableKey}`;
var add = `filter-add-${tableKey}`;
var clear = `filter-clear-${tableKey}`;
var make = `filter-make-${tableKey}`;
var filters = loadTableFilters(tableKey);
var element = $(target);
// One blank slate, please
element.empty();
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-default filter-tag'><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-trash-alt'></span></button>`);
}
for (var key in filters) {
var value = getFilterOptionValue(tableKey, key, filters[key]);
var title = getFilterTitle(tableKey, key);
var description = getFilterDescription(tableKey, key);
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
}
// Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() {
if (!addClicked) {
addClicked = true;
var html = '';
//`<div class='filter-input'>`;
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 += '</div>';
element.append(html);
// Add a callback for when the filter tag selection is changed
element.find(`#filter-tag-${tableKey}`).on('change', function() {
var list = element.find(`#filter-value-${tableKey}`);
list.replaceWith(generateFilterInput(tableKey, this.value));
});
// Add a callback for when the new filter is created
element.find(`#filter-make-${tableKey}`).click(function() {
var tag = element.find(`#filter-tag-${tableKey}`).val();
var val = element.find(`#filter-value-${tableKey}`).val();
// Only add the new filter if it is not empty!
if (tag && tag.length > 0) {
var filters = addTableFilter(tableKey, tag, val);
reloadTableFilters(table, filters);
// Run this function again
setupFilterList(tableKey, table, target);
}
});
} else {
addClicked = false;
setupFilterList(tableKey, table, target);
}
});
// Add a callback for clearing all the filters
element.find(`#${clear}`).click(function() {
var filters = clearTableFilters(tableKey);
reloadTableFilters(table, filters);
setupFilterList(tableKey, table, target);
});
// Add callback for deleting each filter
element.find(".close").click(function(event) {
var me = $(this);
var filter = me.attr(`filter-tag-${tableKey}`);
var filters = removeTableFilter(tableKey, filter);
reloadTableFilters(table, filters);
// Run this function again!
setupFilterList(tableKey, table, target);
});
}
/**
* Return the pretty title for the given table and filter selection.
* If no title is provided, default to the key value.
*
*/
function getFilterTitle(tableKey, filterKey) {
var settings = getFilterSettings(tableKey, filterKey);
return settings.title || filterKey;
}
/**
* Return the pretty description for the given table and filter selection
*/
function getFilterDescription(tableKey, filterKey) {
var settings = getFilterSettings(tableKey, filterKey);
return settings.title;
}
/*
* Return a description for the given table and filter selection.
*/
function getFilterDescription(tableKey, filterKey) {
var settings = getFilterSettings(tableKey, filterKey);
return settings.description || filterKey;
}
/*
* Return the display value for a particular option
*/
function getFilterOptionValue(tableKey, filterKey, valueKey) {
var filter = getFilterSettings(tableKey, filterKey);
var value = String(valueKey);
// Lookup for boolean options
if (filter.type == 'bool') {
if (value == '1') return '{% trans "true" %}';
if (value == '0') return '{% trans "false" %}';
return value;
}
// Iterate through a list of options
if ('options' in filter) {
for (var key in filter.options) {
if (key == valueKey) {
return filter.options[key].value;
}
}
// Could not find a match
return value;
}
// Cannot map to a display string - return the original text
return value;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,249 @@
{% load i18n %}
function printStockItemLabels(items, options={}) {
/**
* Print stock item labels for the given stock items
*/
if (items.length == 0) {
showAlertDialog(
'{% trans "Select Stock Items" %}',
'{% trans "Stock item(s) must be selected before printing labels" %}'
);
return;
}
// Request available labels from the server
inventreeGet(
'{% url "api-stockitem-label-list" %}',
{
enabled: true,
items: items,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Labels Found" %}',
'{% trans "No labels found which match selected stock item(s)" %}',
);
return;
}
// Select label to print
selectLabel(
response,
items,
{
success: function(pk) {
var href = `/api/label/stock/${pk}/print/?`;
items.forEach(function(item) {
href += `items[]=${item}&`;
});
window.location.href = href;
}
}
);
}
}
);
}
function printStockLocationLabels(locations, options={}) {
if (locations.length == 0) {
showAlertDialog(
'{% trans "Select Stock Locations" %}',
'{% trans "Stock location(s) must be selected before printing labels" %}'
);
return;
}
// Request available labels from the server
inventreeGet(
'{% url "api-stocklocation-label-list" %}',
{
enabled: true,
locations: locations,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Labels Found" %}',
'{% trans "No labels found which match selected stock location(s)" %}',
);
return;
}
// Select label to print
selectLabel(
response,
locations,
{
success: function(pk) {
var href = `/api/label/location/${pk}/print/?`;
locations.forEach(function(location) {
href += `locations[]=${location}&`;
});
window.location.href = href;
}
}
);
}
}
)
}
function printPartLabels(parts, options={}) {
/**
* Print labels for the provided parts
*/
if (parts.length == 0) {
showAlertDialog(
'{% trans "Select Parts" %}',
'{% trans "Part(s) must be selected before printing labels" %}',
);
return;
}
// Request available labels from the server
inventreeGet(
'{% url "api-part-label-list" %}',
{
enabled: true,
parts: parts,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Labels Found" %}',
'{% trans "No labels found which match the selected part(s)" %}',
);
return;
}
// Select label to print
selectLabel(
response,
parts,
{
success: function(pk) {
var url = `/api/label/part/${pk}/print/?`;
parts.forEach(function(part) {
url += `parts[]=${part}&`;
});
window.location.href = url;
}
}
);
}
}
);
}
function selectLabel(labels, items, options={}) {
/**
* Present the user with the available labels,
* and allow them to select which label to print.
*
* The intent is that the available labels have been requested
* (via AJAX) from the server.
*/
// If only a single label template is provided,
// just run with that!
if (labels.length == 1) {
if (options.success) {
options.success(labels[0].pk);
}
return;
}
var modal = options.modal || '#modal-form';
var label_list = makeOptionsList(
labels,
function(item) {
var text = item.name;
if (item.description) {
text += ` - ${item.description}`;
}
return text;
},
function(item) {
return item.pk;
}
);
// Construct form
var html = '';
if (items.length > 0) {
html += `
<div class='alert alert-block alert-info'>
${items.length} {% trans "stock items selected" %}
</div>`;
}
html += `
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
<div class='form-group'>
<label class='control-label requiredField' for='id_label'>
{% trans "Select Label" %}
</label>
<div class='controls'>
<select id='id_label' class='select form-control name='label'>
${label_list}
</select>
</div>
</div>
</form>`;
openModal({
modal: modal,
});
modalEnable(modal, true);
modalSetTitle(modal, '{% trans "Select Label Template" %}');
modalSetContent(modal, html);
attachSelect(modal);
modalSubmit(modal, function() {
var label = $(modal).find('#id_label');
var pk = label.val();
closeModal(modal);
if (options.success) {
options.success(pk);
}
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,232 @@
{% load i18n %}
function blankImage() {
return `/static/img/blank_image.png`;
}
// Render a select2 thumbnail image
function select2Thumbnail(image) {
if (!image) {
image = blankImage();
}
return `<img src='${image}' class='select2-thumbnail'>`;
}
/*
* This file contains functions for rendering various InvenTree database models,
* in particular for displaying them in modal forms in a 'select2' context.
*
* Each renderer is provided with three arguments:
*
* - name: The 'name' of the model instance in the referring model
* - data: JSON data which represents the model instance. Returned via a GET request.
* - parameters: The field parameters provided via an OPTIONS request to the endpoint.
* - options: User options provided by the client
*/
// Renderer for "Company" model
function renderCompany(name, data, parameters, options) {
var html = select2Thumbnail(data.image);
html += `<span><b>${data.name}</b></span> - <i>${data.description}</i>`;
html += `<span class='float-right'>{% trans "Company ID" %}: ${data.pk}</span>`;
return html;
}
// Renderer for "StockItem" model
function renderStockItem(name, data, parameters, options) {
var image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
var html = `<img src='${image}' class='select2-thumbnail'>`;
html += ` <span>${data.part_detail.full_name || data.part_detail.name}</span>`;
if (data.serial && data.quantity == 1) {
html += ` - <i>{% trans "Serial Number" %}: ${data.serial}`;
} else {
html += ` - <i>{% trans "Quantity" %}: ${data.quantity}`;
}
if (data.part_detail.description) {
html += `<p><small>${data.part_detail.description}</small></p>`;
}
return html;
}
// Renderer for "StockLocation" model
function renderStockLocation(name, data, parameters, options) {
var level = '- '.repeat(data.level);
var html = `<span>${level}${data.pathstring}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`;
return html;
}
function renderBuild(name, data, parameters, options) {
var image = null;
if (data.part_detail && data.part_detail.thumbnail) {
image = data.part_detail.thumbnail;
}
var html = select2Thumbnail(image);
html += `<span><b>${data.reference}</b></span> - ${data.quantity} x ${data.part_detail.full_name}`;
html += `<span class='float-right'>{% trans "Build ID" %}: ${data.pk}</span>`;
html += `<p><i>${data.title}</i></p>`;
return html;
}
// Renderer for "Part" model
function renderPart(name, data, parameters, options) {
var html = select2Thumbnail(data.image);
html += ` <span>${data.full_name || data.name}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Part ID" %}: ${data.pk}</span>`;
return html;
}
// Renderer for "User" model
function renderUser(name, data, parameters, options) {
var html = `<span>${data.username}</span>`;
if (data.first_name && data.last_name) {
html += ` - <i>${data.first_name} ${data.last_name}</i>`;
}
return html;
}
// Renderer for "Owner" model
function renderOwner(name, data, parameters, options) {
var html = `<span>${data.name}</span>`;
switch (data.label) {
case 'user':
html += `<span class='float-right fas fa-user'></span>`;
break;
case 'group':
html += `<span class='float-right fas fa-users'></span>`;
break;
default:
break;
}
return html;
}
// Renderer for "PartCategory" model
function renderPartCategory(name, data, parameters, options) {
var level = '- '.repeat(data.level);
var html = `<span>${level}${data.pathstring}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`;
return html;
}
function renderPartParameterTemplate(name, data, parameters, options) {
var html = `<span>${data.name} - [${data.units}]</span>`;
return html;
}
// Renderer for "ManufacturerPart" model
function renderManufacturerPart(name, data, parameters, options) {
var manufacturer_image = null;
var part_image = null;
if (data.manufacturer_detail) {
manufacturer_image = data.manufacturer_detail.image;
}
if (data.part_detail) {
part_image = data.part_detail.thumbnail || data.part_detail.image;
}
var html = '';
html += select2Thumbnail(manufacturer_image);
html += select2Thumbnail(part_image);
html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'>{% trans "Manufacturer Part ID" %}: ${data.pk}</span>`;
return html;
}
// Renderer for "SupplierPart" model
function renderSupplierPart(name, data, parameters, options) {
var supplier_image = null;
var part_image = null;
if (data.supplier_detail) {
supplier_image = data.supplier_detail.image;
}
if (data.part_detail) {
part_image = data.part_detail.thumbnail || data.part_detail.image;
}
var html = '';
html += select2Thumbnail(supplier_image);
html += select2Thumbnail(part_image);
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'>{% trans "Supplier Part ID" %}: ${data.pk}</span>`;
return html;
}

View File

@ -0,0 +1,84 @@
/*
* Attach callbacks to navigation bar elements.
*
* Searches for elements with the class 'nav-toggle'.
* A callback is added to each element,
* to display the matching panel.
*
* The 'id' of the .nav-toggle element should be of the form "select-<x>",
* and point to a matching "panel-<x>"
*/
function attachNavCallbacks(options={}) {
$('.nav-toggle').click(function() {
var el = $(this);
// Find the matching "panel" element
var panelName = el.attr('id').replace('select-', '');
activatePanel(panelName, options);
});
var panelClass = options.name || 'unknown';
/* Look for a default panel to initialize
* First preference = URL parameter e.g. ?display=part-stock
* Second preference = localStorage
* Third preference = default
*/
var defaultPanel = $.urlParam('display') || localStorage.getItem(`inventree-selected-panel-${panelClass}`) || options.default;
if (defaultPanel) {
activatePanel(defaultPanel);
}
}
function activatePanel(panelName, options={}) {
var panelClass = options.name || 'unknown';
// First, cause any other panels to "fade out"
$('.panel-visible').hide();
$('.panel-visible').removeClass('panel-visible');
// Find the target panel
var panel = `#panel-${panelName}`;
var select = `#select-${panelName}`;
// Check that the selected panel (and select) exist
if ($(panel).length && $(select).length) {
// Yep, both are displayed
} else {
// Either the select or the panel are not displayed!
// Iterate through the available 'select' elements until one matches
panelName = null;
$('.nav-toggle').each(function(item) {
var panel_name = $(this).attr('id').replace('select-', '');
if ($(`#panel-${panel_name}`).length && (panelName == null)) {
panelName = panel_name;
}
panel = `#panel-${panelName}`;
select = `#select-${panelName}`;
});
}
// Save the selected panel
localStorage.setItem(`inventree-selected-panel-${panelClass}`, panelName);
// Display the panel
$(panel).addClass('panel-visible');
$(panel).fadeIn(100);
// Un-select all selectors
$('.list-group-item').removeClass('active');
// Find the associated selector
var select = `#select-${panelName}`;
$(select).parent('.list-group-item').addClass('active');
}

View File

@ -0,0 +1,467 @@
{% load i18n %}
{% load inventree_extras %}
// Create a new SalesOrder
function createSalesOrder(options={}) {
constructForm('{% url "api-so-list" %}', {
method: 'POST',
fields: {
reference: {
prefix: '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}',
},
customer: {
value: options.customer,
},
customer_reference: {},
description: {},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
responsible: {
icon: 'fa-user',
}
},
onSuccess: function(data) {
location.href = `/order/sales-order/${data.pk}/`;
},
title: '{% trans "Create Sales Order" %}',
});
}
// Create a new PurchaseOrder
function createPurchaseOrder(options={}) {
constructForm('{% url "api-po-list" %}', {
method: 'POST',
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
},
supplier: {
value: options.supplier,
},
supplier_reference: {},
description: {},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
responsible: {
icon: 'fa-user',
}
},
onSuccess: function(data) {
location.href = `/order/purchase-order/${data.pk}/`;
},
title: '{% trans "Create Purchase Order" %}',
});
}
function removeOrderRowFromOrderWizard(e) {
/* Remove a part selection from an order form. */
e = e || window.event;
var src = e.target || e.srcElement;
var row = $(src).attr('row');
$('#' + row).remove();
}
function newSupplierPartFromOrderWizard(e) {
/* Create a new supplier part directly from an order form.
* Launches a secondary modal and (if successful),
* back-populates the selected row.
*/
e = e || window.event;
var src = e.srcElement || e.target;
var part = $(src).attr('part');
console.log('part: ' + part);
if (!part) {
part = $(src).closest('button').attr('part');
console.log('parent: ' + part);
}
launchModalForm("/supplier-part/new/", {
modal: '#modal-form-secondary',
data: {
part: part,
},
success: function(response) {
/* A new supplier part has been created! */
var dropdown = '#id_supplier_part_' + part;
var option = new Option(response.text, response.pk, true, true);
$('#modal-form').find(dropdown).append(option).trigger('change');
},
});
}
function newPurchaseOrderFromOrderWizard(e) {
/* Create a new purchase order directly from an order form.
* Launches a secondary modal and (if successful),
* back-fills the newly created purchase order.
*/
e = e || window.event;
var src = e.target || e.srcElement;
var supplier = $(src).attr('supplierid');
launchModalForm("/order/purchase-order/new/", {
modal: '#modal-form-secondary',
data: {
supplier: supplier,
},
success: function(response) {
/* A new purchase order has been created! */
var dropdown = '#id-purchase-order-' + supplier;
var option = new Option(response.text, response.pk, true, true);
$('#modal-form').find(dropdown).append(option).trigger('change');
},
});
}
function editPurchaseOrderLineItem(e) {
/* Edit a purchase order line item in a modal form.
*/
e = e || window.event;
var src = e.target || e.srcElement;
var url = $(src).attr('url');
launchModalForm(url, {
reload: true,
});
}
function removePurchaseOrderLineItem(e) {
/* Delete a purchase order line item in a modal form
*/
e = e || window.event;
var src = e.target || e.srcElement;
var url = $(src).attr('url');
launchModalForm(url, {
reload: true,
});
}
function loadPurchaseOrderTable(table, options) {
/* Create a purchase-order table */
options.params = options.params || {};
options.params['supplier_detail'] = true;
var filters = loadTableFilters("purchaseorder");
for (var key in options.params) {
filters[key] = options.params[key];
}
options.url = options.url || '{% url "api-po-list" %}';
setupFilterList("purchaseorder", $(table));
$(table).inventreeTable({
url: options.url,
queryParams: filters,
name: 'purchaseorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; },
columns: [
{
title: '',
visible: true,
checkbox: true,
switchable: false,
},
{
field: 'reference',
title: '{% trans "Purchase Order" %}',
sortable: true,
switchable: false,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}";
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, `/order/purchase-order/${row.pk}/`);
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
}
return html;
}
},
{
field: 'supplier_detail',
title: '{% trans "Supplier" %}',
sortable: true,
sortName: 'supplier__name',
formatter: function(value, row, index, field) {
return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`);
}
},
{
field: 'supplier_reference',
title: '{% trans "Supplier Reference" %}',
},
{
field: 'description',
title: '{% trans "Description" %}',
},
{
field: 'status',
title: '{% trans "Status" %}',
sortable: true,
formatter: function(value, row, index, field) {
return purchaseOrderStatusDisplay(row.status, row.status_text);
}
},
{
field: 'creation_date',
title: '{% trans "Date" %}',
sortable: true,
},
{
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
},
{
field: 'line_items',
title: '{% trans "Items" %}',
sortable: true,
},
],
});
}
function loadSalesOrderTable(table, options) {
options.params = options.params || {};
options.params['customer_detail'] = true;
var filters = loadTableFilters("salesorder");
for (var key in options.params) {
filters[key] = options.params[key];
}
options.url = options.url || '{% url "api-so-list" %}';
setupFilterList("salesorder", $(table));
$(table).inventreeTable({
url: options.url,
queryParams: filters,
name: 'salesorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
formatNoMatches: function() { return '{% trans "No sales orders found" %}'; },
columns: [
{
title: '',
checkbox: true,
visible: true,
switchable: false,
},
{
sortable: true,
field: 'reference',
title: '{% trans "Sales Order" %}',
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, `/order/sales-order/${row.pk}/`);
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
}
return html;
},
},
{
sortable: true,
sortName: 'customer__name',
field: 'customer_detail',
title: '{% trans "Customer" %}',
formatter: function(value, row, index, field) {
if (!row.customer_detail) {
return '{% trans "Invalid Customer" %}';
}
return imageHoverIcon(row.customer_detail.image) + renderLink(row.customer_detail.name, `/company/${row.customer}/sales-orders/`);
}
},
{
sortable: true,
field: 'customer_reference',
title: '{% trans "Customer Reference" %}',
},
{
sortable: false,
field: 'description',
title: '{% trans "Description" %}',
},
{
sortable: true,
field: 'status',
title: '{% trans "Status" %}',
formatter: function(value, row, index, field) {
return salesOrderStatusDisplay(row.status, row.status_text);
}
},
{
sortable: true,
field: 'creation_date',
title: '{% trans "Creation Date" %}',
},
{
sortable: true,
field: 'target_date',
title: '{% trans "Target Date" %}',
},
{
sortable: true,
field: 'shipment_date',
title: '{% trans "Shipment Date" %}',
},
{
sortable: true,
field: 'line_items',
title: '{% trans "Items" %}'
},
],
});
}
function loadSalesOrderAllocationTable(table, options={}) {
/**
* Load a table with SalesOrderAllocation items
*/
options.params = options.params || {};
options.params['location_detail'] = true;
options.params['part_detail'] = true;
options.params['item_detail'] = true;
options.params['order_detail'] = true;
var filters = loadTableFilters("salesorderallocation");
for (var key in options.params) {
filters[key] = options.params[key];
}
setupFilterList("salesorderallocation", $(table));
$(table).inventreeTable({
url: '{% url "api-so-allocation-list" %}',
queryParams: filters,
name: 'salesorderallocation',
groupBy: false,
search: false,
paginationVAlign: 'bottom',
original: options.params,
formatNoMatches: function() { return '{% trans "No sales order allocations found" %}'; },
columns: [
{
field: 'pk',
visible: false,
switchable: false,
},
{
field: 'order',
switchable: false,
title: '{% trans "Order" %}',
switchable: false,
formatter: function(value, row) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var ref = `${prefix}${row.order_detail.reference}`;
return renderLink(ref, `/order/sales-order/${row.order}/`);
}
},
{
field: 'item',
title: '{% trans "Stock Item" %}',
formatter: function(value, row) {
// Render a link to the particular stock item
var link = `/stock/item/${row.item}/`;
var text = `{% trans "Stock Item" %} ${row.item}`;
return renderLink(text, link);
}
},
{
field: 'location',
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (!value) {
return '{% trans "Location not specified" %}';
}
var link = `/stock/location/${value}`;
var text = row.location_detail.description;
return renderLink(text, link);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true,
}
]
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,357 @@
{% load i18n %}
function selectReport(reports, items, options={}) {
/**
* Present the user with the available reports,
* and allow them to select which report to print.
*
* The intent is that the available report templates have been requested
* (via AJAX) from the server.
*/
// If there is only a single report available, just print!
if (reports.length == 1) {
if (options.success) {
options.success(reports[0].pk);
}
return;
}
var modal = options.modal || '#modal-form';
var report_list = makeOptionsList(
reports,
function(item) {
var text = item.name;
if (item.description) {
text += ` - ${item.description}`;
}
return text;
},
function(item) {
return item.pk;
}
);
// Construct form
var html = '';
if (items.length > 0) {
html += `
<div class='alert alert-block alert-info'>
${items.length} {% trans "items selected" %}
</div>`;
}
html += `
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
<div class='form-group'>
<label class='control-label requiredField' for='id_report'>
{% trans "Select Report Template" %}
</label>
<div class='controls'>
<select id='id_report' class='select form-control name='report'>
${report_list}
</select>
</div>
</div>
</form>`;
openModal({
modal: modal,
});
modalEnable(modal, true);
modalSetTitle(modal, '{% trans "Select Test Report Template" %}');
modalSetContent(modal, html);
attachSelect(modal);
modalSubmit(modal, function() {
var label = $(modal).find('#id_report');
var pk = label.val();
closeModal(modal);
if (options.success) {
options.success(pk);
}
});
}
function printTestReports(items, options={}) {
/**
* Print test reports for the provided stock item(s)
*/
if (items.length == 0) {
showAlertDialog(
'{% trans "Select Stock Items" %}',
'{% trans "Stock item(s) must be selected before printing reports" %}'
);
return;
}
// Request available reports from the server
inventreeGet(
'{% url "api-stockitem-testreport-list" %}',
{
enabled: true,
items: items,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Reports Found" %}',
'{% trans "No report templates found which match selected stock item(s)" %}',
);
return;
}
// Select report template to print
selectReport(
response,
items,
{
success: function(pk) {
var href = `/api/report/test/${pk}/print/?`;
items.forEach(function(item) {
href += `item=${item}&`;
});
window.location.href = href;
}
}
);
}
}
);
}
function printBuildReports(builds, options={}) {
/**
* Print Build report for the provided build(s)
*/
if (builds.length == 0) {
showAlertDialog(
'{% trans "Select Builds" %}',
'{% trans "Build(s) must be selected before printing reports" %}',
);
return;
}
inventreeGet(
'{% url "api-build-report-list" %}',
{
enabled: true,
builds: builds,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Reports Found" %}',
'{% trans "No report templates found which match selected build(s)" %}'
);
return;
}
// Select which report to print
selectReport(
response,
builds,
{
success: function(pk) {
var href = `/api/report/build/${pk}/print/?`;
builds.forEach(function(build) {
href += `build=${build}&`;
});
window.location.href = href;
}
}
)
}
}
)
}
function printBomReports(parts, options={}) {
/**
* Print BOM reports for the provided part(s)
*/
if (parts.length == 0) {
showAlertDialog(
'{% trans "Select Parts" %}',
'{% trans "Part(s) must be selected before printing reports" %}'
);
return;
}
// Request available reports from the server
inventreeGet(
'{% url "api-bom-report-list" %}',
{
enabled: true,
parts: parts,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Reports Found" %}',
'{% trans "No report templates found which match selected part(s)" %}',
);
return;
}
// Select which report to print
selectReport(
response,
parts,
{
success: function(pk) {
var href = `/api/report/bom/${pk}/print/?`;
parts.forEach(function(part) {
href += `part=${part}&`;
});
window.location.href = href;
}
}
);
}
}
)
}
function printPurchaseOrderReports(orders, options={}) {
/**
* Print PO reports for the provided purchase order(s)
*/
if (orders.length == 0) {
showAlertDialog(
'{% trans "Select Purchase Orders" %}',
'{% trans "Purchase Order(s) must be selected before printing report" %}',
);
return;
}
// Request avaiable report templates
inventreeGet(
'{% url "api-po-report-list" %}',
{
enabled: true,
orders: orders,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Reports Found" %}',
'{% trans "No report templates found which match selected orders" %}',
);
return;
}
// Select report template
selectReport(
response,
orders,
{
success: function(pk) {
var href = `/api/report/po/${pk}/print/?`;
orders.forEach(function(order) {
href += `order=${order}&`;
});
window.location.href = href;
}
}
)
}
}
)
}
function printSalesOrderReports(orders, options={}) {
/**
* Print SO reports for the provided purchase order(s)
*/
if (orders.length == 0) {
showAlertDialog(
'{% trans "Select Sales Orders" %}',
'{% trans "Sales Order(s) must be selected before printing report" %}',
);
return;
}
// Request avaiable report templates
inventreeGet(
'{% url "api-so-report-list" %}',
{
enabled: true,
orders: orders,
},
{
success: function(response) {
if (response.length == 0) {
showAlertDialog(
'{% trans "No Reports Found" %}',
'{% trans "No report templates found which match selected orders" %}',
);
return;
}
// Select report template
selectReport(
response,
orders,
{
success: function(pk) {
var href = `/api/report/so/${pk}/print/?`;
orders.forEach(function(order) {
href += `order=${order}&`;
});
window.location.href = href;
}
}
)
}
}
)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,363 @@
{% load i18n %}
{% load status_codes %}
{% load inventree_extras %}
{% include "status_codes.html" with label='stock' options=StockStatus.list %}
{% include "status_codes.html" with label='stockHistory' options=StockHistoryCode.list %}
{% include "status_codes.html" with label='build' options=BuildStatus.list %}
{% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %}
{% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %}
function getAvailableTableFilters(tableKey) {
tableKey = tableKey.toLowerCase();
// Filters for "variant" table
if (tableKey == "variants") {
return {
active: {
type: 'bool',
title: '{% trans "Active" %}',
},
template: {
type: 'bool',
title: '{% trans "Template" %}',
},
virtual: {
type: 'bool',
title: '{% trans "Virtual" %}',
},
trackable: {
type: 'bool',
title: '{% trans "Trackable" %}',
},
};
}
// Filters for Bill of Materials table
if (tableKey == "bom") {
return {
sub_part_trackable: {
type: 'bool',
title: '{% trans "Trackable Part" %}'
},
sub_part_assembly: {
type: 'bool',
title: '{% trans "Assembled Part" %}',
},
validated: {
type: 'bool',
title: '{% trans "Validated" %}',
},
inherited: {
type: 'bool',
title: '{% trans "Inherited" %}',
},
allow_variants: {
type: 'bool',
title: '{% trans "Allow Variant Stock" %}',
}
};
}
// Filters for the "used in" table
if (tableKey == 'usedin') {
return {
'part_active': {
type: 'bool',
title: '{% trans "Active" %}',
},
};
}
// Filters for "stock location" table
if (tableKey == "location") {
return {
cascade: {
type: 'bool',
title: '{% trans "Include sublocations" %}',
description: '{% trans "Include locations" %}',
}
};
}
// Filters for "part category" table
if (tableKey == "category") {
return {
cascade: {
type: 'bool',
title: '{% trans "Include subcategories" %}',
description: '{% trans "Include subcategories" %}',
}
};
}
// Filters for the "customer stock" table (really a subset of "stock")
if (tableKey == "customerstock") {
return {
serialized: {
type: 'bool',
title: '{% trans "Is Serialized" %}',
},
serial_gte: {
title: '{% trans "Serial number GTE" %}',
description: '{% trans "Serial number greater than or equal to" %}'
},
serial_lte: {
title: '{% trans "Serial number LTE" %}',
description: '{% trans "Serial number less than or equal to" %}',
},
serial: {
title: '{% trans "Serial number" %}',
description: '{% trans "Serial number" %}'
},
batch: {
title: '{% trans "Batch" %}',
description: '{% trans "Batch code" %}',
},
};
}
// Filters for the "Stock" table
if (tableKey == 'stock') {
return {
active: {
type: 'bool',
title: '{% trans "Active parts" %}',
description: '{% trans "Show stock for active parts" %}',
},
assembly: {
type: 'bool',
title: '{% trans "Assembly" %}',
description: '{% trans "Part is an assembly" %}',
},
allocated: {
type: 'bool',
title: '{% trans "Is allocated" %}',
description: '{% trans "Item has been allocated" %}',
},
cascade: {
type: 'bool',
title: '{% trans "Include sublocations" %}',
description: '{% trans "Include stock in sublocations" %}',
},
depleted: {
type: 'bool',
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
expired: {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
},
stale: {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
},
{% endif %}
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',
description: '{% trans "Show items which are in stock" %}',
},
is_building: {
type: 'bool',
title: '{% trans "In Production" %}',
description: '{% trans "Show items which are in production" %}',
},
include_variants: {
type: 'bool',
title: '{% trans "Include Variants" %}',
description: '{% trans "Include stock items for variant parts" %}',
},
installed: {
type: 'bool',
title: '{% trans "Installed" %}',
description: '{% trans "Show stock items which are installed in another item" %}',
},
sent_to_customer: {
type: 'bool',
title: '{% trans "Sent to customer" %}',
description: '{% trans "Show items which have been assigned to a customer" %}',
},
serialized: {
type: 'bool',
title: '{% trans "Is Serialized" %}',
},
serial: {
title: '{% trans "Serial number" %}',
description: '{% trans "Serial number" %}'
},
serial_gte: {
title: '{% trans "Serial number GTE" %}',
description: '{% trans "Serial number greater than or equal to" %}'
},
serial_lte: {
title: '{% trans "Serial number LTE" %}',
description: '{% trans "Serial number less than or equal to" %}',
},
status: {
options: stockCodes,
title: '{% trans "Stock status" %}',
description: '{% trans "Stock status" %}',
},
batch: {
title: '{% trans "Batch" %}',
description: '{% trans "Batch code" %}',
},
has_purchase_price: {
type: 'bool',
title: '{% trans "Has purchase price" %}',
description: '{% trans "Show stock items which have a purchase price set" %}',
},
};
}
// Filters for the 'stock test' table
if (tableKey == 'stocktests') {
return {
result: {
type: 'bool',
title: '{% trans "Test result" %}',
},
};
}
// Filters for the 'part test template' table
if (tableKey == 'parttests') {
return {
required: {
type: 'bool',
title: '{% trans "Required" %}',
}
};
}
// Filters for the "Build" table
if (tableKey == 'build') {
return {
status: {
title: '{% trans "Build status" %}',
options: buildCodes,
},
active: {
type: 'bool',
title: '{% trans "Active" %}',
},
overdue: {
type: 'bool',
title: '{% trans "Overdue" %}',
},
};
}
// Filters for the "Order" table
if (tableKey == "purchaseorder") {
return {
status: {
title: '{% trans "Order status" %}',
options: purchaseOrderCodes,
},
outstanding: {
type: 'bool',
title: '{% trans "Outstanding" %}',
},
overdue: {
type: 'bool',
title: '{% trans "Overdue" %}',
},
};
}
if (tableKey == "salesorder") {
return {
status: {
title: '{% trans "Order status" %}',
options: salesOrderCodes,
},
outstanding: {
type: 'bool',
title: '{% trans "Outstanding" %}',
},
overdue: {
type: 'bool',
title: '{% trans "Overdue" %}',
},
};
}
if (tableKey == 'supplier-part') {
return {
active: {
type: 'bool',
title: '{% trans "Active parts" %}',
}
};
}
// Filters for the "Parts" table
if (tableKey == "parts") {
return {
cascade: {
type: 'bool',
title: '{% trans "Include subcategories" %}',
description: '{% trans "Include parts in subcategories" %}',
},
has_ipn: {
type: 'bool',
title: '{% trans "Has IPN" %}',
description: '{% trans "Part has internal part number" %}',
},
active: {
type: 'bool',
title: '{% trans "Active" %}',
description: '{% trans "Show active parts" %}',
},
is_template: {
type: 'bool',
title: '{% trans "Template" %}',
},
has_stock: {
type: 'bool',
title: '{% trans "Stock available" %}'
},
low_stock: {
type: 'bool',
title: '{% trans "Low stock" %}',
},
assembly: {
type: 'bool',
title: '{% trans "Assembly" %}',
},
component: {
type: 'bool',
title: '{% trans "Component" %}',
},
starred: {
type: 'bool',
title: '{% trans "Starred" %}',
},
salable: {
type: 'bool',
title: '{% trans "Salable" %}',
},
trackable: {
type: 'bool',
title: '{% trans "Trackable" %}',
},
purchaseable: {
type: 'bool',
title: '{% trans "Purchasable" %}',
},
};
}
// Finally, no matching key
return {};
}

View File

@ -0,0 +1,375 @@
{% load i18n %}
function reloadtable(table) {
$(table).bootstrapTable('refresh');
}
function editButton(url, text='Edit') {
return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
}
function deleteButton(url, text='Delete') {
return "<button class='btn btn-danger delete-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
}
function renderLink(text, url, options={}) {
if (url === null || url === undefined || url === '') {
return text;
}
var max_length = options.max_length || -1;
var remove_http = options.remove_http || false;
// Shorten the displayed length if required
if ((max_length > 0) && (text.length > max_length)) {
var slice_length = (max_length - 3) / 2;
var text_start = text.slice(0, slice_length);
var text_end = text.slice(-slice_length);
text = `${text_start}...${text_end}`;
}
return '<a href="' + url + '">' + text + '</a>';
}
function enableButtons(elements, enabled) {
for (let item of elements) {
$(item).prop('disabled', !enabled);
}
}
function linkButtonsToSelection(table, buttons) {
/* Link a bootstrap-table object to one or more buttons.
* The buttons will only be enabled if there is at least one row selected
*/
if (typeof table === 'string') {
table = $(table);
}
// Initially set the enable state of the buttons
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
// Add a callback
table.on('check.bs.table uncheck.bs.table check-some.bs.table uncheck-some.bs.table check-all.bs.table uncheck-all.bs.table', function(row) {
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
});
}
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
/*
* Reload a table which has already been made into a bootstrap table.
* New filters can be optionally provided, to change the query params.
*/
function reloadTableFilters(table, filters) {
// Simply perform a refresh
if (filters == null) {
table.bootstrapTable('refresh');
return;
}
// More complex refresh with new filters supplied
var options = table.bootstrapTable('getOptions');
// Construct a new list of filters to use for the query
var params = {};
for (var key in filters) {
params[key] = filters[key];
}
// Original query params will override
if (options.original != null) {
for (var key in options.original) {
params[key] = options.original[key];
}
}
options.queryParams = function(tableParams) {
return convertQueryParameters(tableParams, params);
};
table.bootstrapTable('refreshOptions', options);
table.bootstrapTable('refresh', filters);
}
function visibleColumnString(columns) {
/* Generate a list of "visible" columns to save to file. */
var fields = [];
columns.forEach(function(column) {
if (column.switchable && column.visible) {
fields.push(column.field);
}
});
return fields.join(',');
}
/*
* Convert bootstrap-table style parameters to "InvenTree" style
*/
function convertQueryParameters(params, filters) {
// Override the way that we ask the server to sort results
// It seems bootstrap-table does not offer a "native" way to do this...
if ('sort' in params) {
var order = params['order'];
var ordering = params['sort'] || null;
if (ordering) {
if (order == 'desc') {
ordering = `-${ordering}`;
}
params['ordering'] = ordering;
}
delete params['sort'];
delete params['order'];
}
for (var key in filters) {
params[key] = filters[key];
}
// Add "order" back in (if it was originally specified by InvenTree)
// Annoyingly, "order" shadows some field names in InvenTree...
if ('order' in filters) {
params['order'] = filters['order'];
}
// Remove searchable[] array (generated by bootstrap-table)
if ('searchable' in params) {
delete params['searchable'];
}
if ('sortable' in params) {
delete params['sortable'];
}
return params;
}
/* Wrapper function for bootstrapTable.
* Sets some useful defaults, and manage persistent settings.
*/
$.fn.inventreeTable = function(options) {
var table = this;
var tableName = options.name || 'table';
var varName = tableName + '-pagesize';
// Pagingation options (can be server-side or client-side as specified by the caller)
if (!options.disablePagination) {
options.pagination = true;
options.paginationVAlign = options.paginationVAlign || 'both';
options.pageSize = inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all'];
options.totalField = 'count';
options.dataField = 'results';
}
// Extract query params
var filters = options.queryParams || options.filters || {};
options.queryParams = function(params) {
return convertQueryParameters(params, filters);
};
options.rememberOrder = true;
if (options.sortable == null) {
options.sortable = true;
}
if (options.search == null) {
options.search = true;
}
if (options.showColumns == null) {
options.showColumns = true;
}
// Callback to save pagination data
options.onPageChange = function(number, size) {
inventreeSave(varName, size);
};
// Callback when a column is changed
options.onColumnSwitch = function(field, checked) {
var columns = table.bootstrapTable('getVisibleColumns');
var text = visibleColumnString(columns);
// Save visible columns
inventreeSave(`table_columns_${tableName}`, text);
};
// Standard options for all tables
table.bootstrapTable(options);
// Load visible column list from memory
// Load visible column list
var visibleColumns = inventreeLoad(`table_columns_${tableName}`, null);
// If a set of visible columns has been saved, load!
if (visibleColumns) {
var columns = visibleColumns.split(",");
// Which columns are currently visible?
var visible = table.bootstrapTable('getVisibleColumns');
if (visible && Array.isArray(visible)) {
visible.forEach(function(column) {
// Visible field should *not* be visible! (hide it!)
if (column.switchable && !columns.includes(column.field)) {
table.bootstrapTable('hideColumn', column.field);
}
});
} else {
console.log('Could not get list of visible columns!');
}
}
// Optionally, link buttons to the table selection
if (options.buttons) {
linkButtonsToSelection(table, options.buttons);
}
}
function customGroupSorter(sortName, sortOrder, sortData) {
var order = sortOrder === 'desc' ? -1 : 1;
sortData.sort(function(a, b) {
// Extract default field values
// Allow multi-level access if required
// Ref: https://stackoverflow.com/a/6394168
function extract(obj, i) {
return obj[i];
}
var aa = sortName.split('.').reduce(extract, a);
var bb = sortName.split('.').reduce(extract, b);
// Extract parent information
var aparent = a._data && a._data['parent-index'];
var bparent = b._data && b._data['parent-index'];
// If either of the comparisons are in a group
if (aparent || bparent) {
// If the parents are different (or one item does not have a parent,
// then we need to extract the parent value for the selected column.
if (aparent != bparent) {
if (aparent) {
aa = a._data['table'].options.groupByFormatter(sortName, 0, a._data['group-data']);
}
if (bparent) {
bb = b._data['table'].options.groupByFormatter(sortName, 0, b._data['group-data']);
}
}
}
if (aa === undefined || aa === null) {
aa = '';
}
if (bb === undefined || bb === null) {
bb = '';
}
if (isNumeric(aa) && isNumeric(bb)) {
if (aa < bb) {
return order * -1;
} else if (aa > bb) {
return order;
} else {
return 0;
}
}
aa = aa.toString();
bb = bb.toString();
var cmp = aa.localeCompare(bb);
if (cmp === -1) {
return order * -1;
} else if (cmp === 1) {
return order;
} else {
return 0;
}
});
}
// Expose default bootstrap table string literals to translation layer
(function ($) {
'use strict';
$.fn.bootstrapTable.locales['en-US-custom'] = {
formatLoadingMessage: function () {
return '{% trans "Loading data" %}';
},
formatRecordsPerPage: function (pageNumber) {
return `${pageNumber} {% trans "rows per page" %}`;
},
formatShowingRows: function (pageFrom, pageTo, totalRows) {
return `{% trans "Showing" %} ${pageFrom} {% trans "to" %} ${pageTo} {% trans "of" %} ${totalRows} {% trans "rows" %}`;
},
formatSearch: function () {
return '{% trans "Search" %}';
},
formatNoMatches: function () {
return '{% trans "No matching results" %}';
},
formatPaginationSwitch: function () {
return '{% trans "Hide/Show pagination" %}';
},
formatRefresh: function () {
return '{% trans "Refresh" %}';
},
formatToggle: function () {
return '{% trans "Toggle" %}';
},
formatColumns: function () {
return '{% trans "Columns" %}';
},
formatAllRows: function () {
return '{% trans "All" %}';
}
};
$.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['en-US-custom']);
})(jQuery);