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/InvenTree/version.py b/InvenTree/InvenTree/version.py index f913678a9b..52a2fbc70e 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -8,7 +8,7 @@ import re import common.models -INVENTREE_SW_VERSION = "0.5.3" +INVENTREE_SW_VERSION = "0.5.4" # InvenTree API version INVENTREE_API_VERSION = 12 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 0b5ae3fcc1..ea6c4924c7 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 ) @@ -504,8 +503,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 90dfecf26a..81d4880246 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -108,7 +108,6 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() price = InvenTreeMoneySerializer( - max_digits=19, decimal_places=4, allow_null=True ) @@ -133,7 +132,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 70dd55a4eb..c6abb0e8fb 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -146,7 +146,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 0e815f8c6d..802147e1df 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) { @@ -730,7 +754,8 @@ function updateFieldValues(fields, options) { function updateFieldValue(name, value, field, options) { - var el = $(options.modal).find(`#id_${name}`); + + var el = getFormFieldElement(name, options); switch (field.type) { case 'boolean': @@ -753,6 +778,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 * @@ -764,7 +829,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; @@ -981,7 +1046,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); @@ -1187,7 +1254,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) { @@ -1342,7 +1409,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); @@ -1363,9 +1430,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, @@ -1770,8 +1835,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) {