diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 0d21550f00..d2d00a932c 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -36,6 +36,13 @@ class InvenTreeMoneySerializer(MoneyField): Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py """ + def __init__(self, *args, **kwargs): + + kwargs["max_digits"] = kwargs.get("max_digits", 19) + kwargs["decimal_places"] = kwargs.get("decimal_places", 4) + + super().__init__(*args, **kwargs) + def get_value(self, data): """ Test that the returned amount is a valid Decimal @@ -52,7 +59,7 @@ class InvenTreeMoneySerializer(MoneyField): amount = Decimal(amount) except: raise ValidationError({ - self.field_name: _("Must be a valid number") + self.field_name: [_("Must be a valid number")], }) currency = data.get(get_currency_field_name(self.field_name), self.default_currency) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index a471b87a90..719d94b0a6 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -9,6 +9,7 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount from InvenTree.serializers import InvenTreeModelSerializer +from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeImageSerializerField from part.serializers import PartBriefSerializer @@ -256,7 +257,11 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() - price = serializers.CharField() + price = InvenTreeMoneySerializer( + allow_null=True, + required=True, + label=_('Price'), + ) price_currency = serializers.ChoiceField( choices=currency_code_mappings(), diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index c872e558a0..992d12d1d9 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -153,7 +153,6 @@ class POLineItemSerializer(InvenTreeModelSerializer): supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) purchase_price = InvenTreeMoneySerializer( - max_digits=19, decimal_places=4, allow_null=True ) @@ -556,8 +555,6 @@ class SOLineItemSerializer(InvenTreeModelSerializer): fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True) sale_price = InvenTreeMoneySerializer( - max_digits=19, - decimal_places=4, allow_null=True ) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 869145e1af..ff1fb2c8c6 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -109,7 +109,6 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() price = InvenTreeMoneySerializer( - max_digits=19, decimal_places=4, allow_null=True ) @@ -134,7 +133,6 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() price = InvenTreeMoneySerializer( - max_digits=19, decimal_places=4, allow_null=True ) 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/forms.js b/InvenTree/templates/js/translated/forms.js index 2818b00534..b459681d4d 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, @@ -1929,8 +1994,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) {