diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index c44dffe94f..d97e81331a 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -148,7 +148,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
purchase_price = InvenTreeMoneySerializer(
label=_('Purchase Price'),
- max_digits=19, decimal_places=4,
allow_null=True
)
diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js
index 53429effea..6b109e2d18 100644
--- a/InvenTree/templates/js/translated/filters.js
+++ b/InvenTree/templates/js/translated/filters.js
@@ -283,6 +283,11 @@ function setupFilterList(tableKey, table, target) {
element.append(`
`);
+ // Callback for reloading the table
+ element.find(`#reload-${tableKey}`).click(function() {
+ $(table).bootstrapTable('refresh');
+ });
+
// If there are no filters defined for this table, exit now
if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
return;
@@ -303,11 +308,6 @@ function setupFilterList(tableKey, table, target) {
element.append(`
${title} = ${value}x
`);
}
- // Callback for reloading the table
- element.find(`#reload-${tableKey}`).click(function() {
- $(table).bootstrapTable('refresh');
- });
-
// Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() {
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index db2c8e46cc..a338310fd1 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -621,6 +621,10 @@ function submitFormData(fields, options) {
var has_files = false;
+ var data_valid = true;
+
+ var data_errors = {};
+
// Extract values for each field
for (var idx = 0; idx < options.field_names.length; idx++) {
@@ -633,6 +637,21 @@ function submitFormData(fields, options) {
if (field) {
+ switch (field.type) {
+ // Ensure numerical fields are "valid"
+ case 'integer':
+ case 'float':
+ case 'decimal':
+ if (!validateFormField(name, options)) {
+ data_valid = false;
+
+ data_errors[name] = ['{% trans "Enter a valid number" %}'];
+ }
+ break;
+ default:
+ break;
+ }
+
var value = getFormFieldValue(name, field, options);
// Handle file inputs
@@ -662,6 +681,11 @@ function submitFormData(fields, options) {
}
}
+ if (!data_valid) {
+ handleFormErrors(data_errors, fields, options);
+ return;
+ }
+
var upload_func = inventreePut;
if (has_files) {
@@ -732,7 +756,8 @@ function updateFieldValues(fields, options) {
* Update the value of a named field
*/
function updateFieldValue(name, value, field, options) {
- var el = $(options.modal).find(`#id_${name}`);
+
+ var el = getFormFieldElement(name, options);
if (!el) {
console.log(`WARNING: updateFieldValue could not find field '${name}'`);
@@ -760,6 +785,46 @@ function updateFieldValue(name, value, field, options) {
}
+// Find the named field element in the modal DOM
+function getFormFieldElement(name, options) {
+
+ var el = $(options.modal).find(`#id_${name}`);
+
+ if (!el.exists) {
+ console.log(`ERROR: Could not find form element for field '${name}'`);
+ }
+
+ return el;
+}
+
+
+/*
+ * Check that a "numerical" input field has a valid number in it.
+ * An invalid number is expunged at the client side by the getFormFieldValue() function,
+ * which means that an empty string '' is sent to the server if the number is not valud.
+ * This can result in confusing error messages displayed under the form field.
+ *
+ * So, we can invalid numbers and display errors *before* the form is submitted!
+ */
+function validateFormField(name, options) {
+
+ if (getFormFieldElement(name, options)) {
+
+ var el = document.getElementById(`id_${name}`);
+
+ if (el.validity.valueMissing) {
+ // Accept empty strings (server will validate)
+ return true;
+ } else {
+ return el.validity.valid;
+ }
+ } else {
+ return false;
+ }
+
+}
+
+
/*
* Extract and field value before sending back to the server
*
@@ -771,7 +836,7 @@ function updateFieldValue(name, value, field, options) {
function getFormFieldValue(name, field, options) {
// Find the HTML element
- var el = $(options.modal).find(`#id_${name}`);
+ var el = getFormFieldElement(name, options);
if (!el) {
return null;
@@ -1086,7 +1151,9 @@ function addFieldCallbacks(fields, options) {
function addFieldCallback(name, field, options) {
- $(options.modal).find(`#id_${name}`).change(function() {
+ var el = getFormFieldElement(name, options);
+
+ el.change(function() {
var value = getFormFieldValue(name, field, options);
@@ -1299,7 +1366,7 @@ function initializeRelatedField(field, fields, options) {
}
// Find the select element and attach a select2 to it
- var select = $(options.modal).find(`#id_${name}`);
+ var select = getFormFieldElement(name, options);
// Add a button to launch a 'secondary' modal
if (field.secondary != null) {
@@ -1492,7 +1559,7 @@ function initializeRelatedField(field, fields, options) {
*/
function setRelatedFieldData(name, data, options) {
- var select = $(options.modal).find(`#id_${name}`);
+ var select = getFormFieldElement(name, options);
var option = new Option(name, data.pk, true, true);
@@ -1513,9 +1580,7 @@ function setRelatedFieldData(name, data, options) {
function initializeChoiceField(field, fields, options) {
- var name = field.name;
-
- var select = $(options.modal).find(`#id_${name}`);
+ var select = getFormFieldElement(field.name, options);
select.select2({
dropdownAutoWidth: false,
@@ -1926,8 +1991,17 @@ function constructInputOptions(name, classes, type, parameters) {
opts.push(`placeholder='${parameters.placeholder}'`);
}
- if (parameters.type == 'boolean') {
+ switch (parameters.type) {
+ case 'boolean':
opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`);
+ break;
+ case 'integer':
+ case 'float':
+ case 'decimal':
+ opts.push(`step='any'`);
+ break;
+ default:
+ break;
}
if (parameters.multiline) {
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index 67fef0b853..75f71e3c4c 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -864,6 +864,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
},
{
sortable: true,
+ switchable: false,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
@@ -879,18 +880,29 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
field: 'purchase_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
- return row.purchase_price_string || row.purchase_price;
+ var formatter = new Intl.NumberFormat(
+ 'en-US',
+ {
+ style: 'currency',
+ currency: row.purchase_price_currency
+ }
+ );
+ return formatter.format(row.purchase_price);
}
},
{
field: 'total_price',
sortable: true,
- field: 'total_price',
- title: '{% trans "Total price" %}',
+ title: '{% trans "Total Price" %}',
formatter: function(value, row) {
- var total = row.purchase_price * row.quantity;
- var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
- return formatter.format(total);
+ var formatter = new Intl.NumberFormat(
+ 'en-US',
+ {
+ style: 'currency',
+ currency: row.purchase_price_currency
+ }
+ );
+ return formatter.format(row.purchase_price * row.quantity);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
@@ -1436,7 +1448,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
- switchable: false,
+ switchable: true,
},
{
sortable: true,
@@ -1456,14 +1468,6 @@ function loadSalesOrderLineItemTable(table, options={}) {
field: 'sale_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
- return row.sale_price_string || row.sale_price;
- }
- },
- {
- sortable: true,
- title: '{% trans "Total price" %}',
- formatter: function(value, row) {
- var total = row.sale_price * row.quantity;
var formatter = new Intl.NumberFormat(
'en-US',
{
@@ -1472,7 +1476,23 @@ function loadSalesOrderLineItemTable(table, options={}) {
}
);
- return formatter.format(total);
+ return formatter.format(row.sale_price);
+ }
+ },
+ {
+ field: 'total_price',
+ sortable: true,
+ title: '{% trans "Total Price" %}',
+ formatter: function(value, row) {
+ var formatter = new Intl.NumberFormat(
+ 'en-US',
+ {
+ style: 'currency',
+ currency: row.sale_price_currency
+ }
+ );
+
+ return formatter.format(row.sale_price * row.quantity);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
@@ -1544,6 +1564,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
if (pending) {
columns.push({
field: 'buttons',
+ switchable: false,
formatter: function(value, row, index, field) {
var html = `
`;