diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index d2fb7b6ff4..4d8ab9ef82 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -11,6 +11,8 @@ from django.core import validators from django import forms from decimal import Decimal +from InvenTree.helpers import normalize + class InvenTreeURLFormField(FormURLField): """ Custom URL form field with custom scheme validators """ @@ -53,7 +55,7 @@ class RoundingDecimalFormField(forms.DecimalField): """ if type(value) == Decimal: - return value.normalize() + return normalize(value) else: return value diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 6b29adb4c4..6b619b4aa2 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -8,6 +8,8 @@ import json import os.path from PIL import Image +from decimal import Decimal + from wsgiref.util import FileWrapper from django.http import StreamingHttpResponse from django.core.exceptions import ValidationError @@ -104,6 +106,20 @@ def isNull(text): return str(text).strip().lower() in ['top', 'null', 'none', 'empty', 'false', '-1', ''] +def normalize(d): + """ + Normalize a decimal number, and remove exponential formatting. + """ + + if type(d) is not Decimal: + d = Decimal(d) + + d = d.normalize() + + # Ref: https://docs.python.org/3/library/decimal.html + return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() + + def decimal2string(d): """ Format a Decimal number as a string, @@ -117,6 +133,9 @@ def decimal2string(d): A string representation of the input number """ + if type(d) is Decimal: + d = normalize(d) + try: # Ensure that the provided string can actually be converted to a float float(d) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 0725c9d1a6..3724835621 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -125,6 +125,8 @@ .label-right { float: right; + margin-left: 3px; + margin-right: 3px; } /* Bootstrap table overrides */ @@ -157,6 +159,66 @@ font-style: italic; } +.dropdown { + padding-left: 1px; + margin-left: 1px; +} + +/* Styles for table buttons and filtering */ +.button-toolbar .btn { + margin-left: 1px; + margin-right: 1px; +} + +.filter-list { + display: inline-block; + *display: inline; + margin-bottom: 1px; + margin-top: 1px; + vertical-align: middle; + margin: 1px; + padding: 2px; + background: #eee; + border: 1px solid #eee; + border-radius: 3px; +} + +.filter-list .close { + cursor: pointer; + right: 0%; + padding-right: 2px; + padding-left: 2px; + transform: translate(0%, -25%); +} + +.filter-list .close:hover {background: #bbb;} + +.filter-tag { + display: inline-block; + *display: inline; + zoom: 1; + padding-left: 3px; + padding-right: 3px; + padding-top: 2px; + padding-bottom: 2px; + border: 1px solid #aaa; + border-radius: 3px; + background: #eee; + margin: 1px; + margin-left: 5px; + margin-right: 5px; +} + +.filter-input { + display: inline-block; + *display: inline; + zoom: 1; +} + +.filter-tag:hover { + background: #ddd; +} + /* Part image icons with full-display on mouse hover */ .hover-img-thumb { diff --git a/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table.js b/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table.js index 4567f29ac9..5fdfd2ee58 100644 --- a/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table.js +++ b/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table.js @@ -3272,10 +3272,7 @@ }, { key: 'getOptions', value: function getOptions() { - // deep copy and remove data - var options = JSON.parse(JSON.stringify(this.options)); - delete options.data; - return options; + return this.options; } }, { key: 'getSelections', diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 1b74088abf..3d1e8fc594 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -133,11 +133,11 @@ function loadBomTable(table, options) { title: 'Part', sortable: true, formatter: function(value, row, index, field) { - var html = imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url); + var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url); // Display an extra icon if this part is an assembly if (row.sub_part_detail.assembly) { - html += ""; + html += ""; } return html; diff --git a/InvenTree/InvenTree/static/script/inventree/build.js b/InvenTree/InvenTree/static/script/inventree/build.js index 8ec872bbcb..e61fa397a6 100644 --- a/InvenTree/InvenTree/static/script/inventree/build.js +++ b/InvenTree/InvenTree/static/script/inventree/build.js @@ -1,3 +1,77 @@ +function loadBuildTable(table, options) { + + var params = options.params || {}; + + var filters = loadTableFilters("build"); + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("build", table); + + table.inventreeTable({ + method: 'get', + formatNoMatches: function() { + return "No builds matching query"; + }, + url: options.url, + queryParams: filters, + groupBy: false, + original: params, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + }, + { + field: 'title', + title: 'Build', + sortable: true, + formatter: function(value, row, index, field) { + return renderLink(value, '/build/' + row.pk + '/'); + } + }, + { + field: 'part', + title: 'Part', + sortable: true, + formatter: function(value, row, index, field) { + + var name = row.part_detail.full_name; + + return imageHoverIcon(row.part_detail.thumbnail) + renderLink(name, '/part/' + row.part + '/'); + } + }, + { + field: 'quantity', + title: 'Quantity', + sortable: true, + }, + { + field: 'status', + title: 'Status', + sortable: true, + formatter: function(value, row, index, field) { + return buildStatusDisplay(value); + }, + }, + { + field: 'creation_date', + title: 'Created', + sortable: true, + }, + { + field: 'completion_date', + title: 'Completed', + sortable: true, + }, + ], + }); +} + + function updateAllocationTotal(id, count, required) { count = parseFloat(count); diff --git a/InvenTree/InvenTree/static/script/inventree/filters.js b/InvenTree/InvenTree/static/script/inventree/filters.js new file mode 100644 index 0000000000..f59645bf8a --- /dev/null +++ b/InvenTree/InvenTree/static/script/inventree/filters.js @@ -0,0 +1,410 @@ +/** + * 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", + build: "", + parts: "cascade=1", + }; +} + + +/** + * 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('&'); + + console.log(`Saving filters for table '${tableKey}' - ${filterstring}`); + + 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: 'true', + }, + '0': { + key: '0', + value: 'false', + }, + }; + } else if ('options' in settings) { + return settings.options; + } + + return null; +} + + +/* + * Generate a list of