mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 21:38:48 +00:00
Pint unit fix (#5381)
* Accept conversion of fractional values - e.g. "1/10" is a valid input value - pint dimensions returns strange results sometimes * Add option (default) to return value without units - Handles conversion for "stringish" input values * Update unit tests * Fix return from convert_physical_value method * Update unit tests * Improved checking for conversion code * Call to_base_units first * Conversion depends on whether units are supplied or not * Updates to unit testing * Handle conversion of units for supplier parts - Includes some refactoring
This commit is contained in:
parent
7394ddae33
commit
647c3ade20
@ -70,12 +70,13 @@ def reload_unit_registry():
|
|||||||
return reg
|
return reg
|
||||||
|
|
||||||
|
|
||||||
def convert_physical_value(value: str, unit: str = None):
|
def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||||
"""Validate that the provided value is a valid physical quantity.
|
"""Validate that the provided value is a valid physical quantity.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
value: Value to validate (str)
|
value: Value to validate (str)
|
||||||
unit: Optional unit to convert to, and validate against
|
unit: Optional unit to convert to, and validate against
|
||||||
|
strip_units: If True, strip units from the returned value, and return only the dimension
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: If the value is invalid or cannot be converted to the specified unit
|
ValidationError: If the value is invalid or cannot be converted to the specified unit
|
||||||
@ -100,7 +101,7 @@ def convert_physical_value(value: str, unit: str = None):
|
|||||||
|
|
||||||
if unit:
|
if unit:
|
||||||
|
|
||||||
if val.units == ureg.dimensionless:
|
if is_dimensionless(val):
|
||||||
# If the provided value is dimensionless, assume that the unit is correct
|
# If the provided value is dimensionless, assume that the unit is correct
|
||||||
val = ureg.Quantity(value, unit)
|
val = ureg.Quantity(value, unit)
|
||||||
else:
|
else:
|
||||||
@ -109,19 +110,65 @@ def convert_physical_value(value: str, unit: str = None):
|
|||||||
|
|
||||||
# At this point we *should* have a valid pint value
|
# At this point we *should* have a valid pint value
|
||||||
# To double check, look at the maginitude
|
# To double check, look at the maginitude
|
||||||
float(val.magnitude)
|
float(ureg.Quantity(val.magnitude).magnitude)
|
||||||
except (TypeError, ValueError, AttributeError):
|
except (TypeError, ValueError, AttributeError):
|
||||||
error = _('Provided value is not a valid number')
|
error = _('Provided value is not a valid number')
|
||||||
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
|
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
|
||||||
error = _('Provided value has an invalid unit')
|
error = _('Provided value has an invalid unit')
|
||||||
except pint.errors.DimensionalityError:
|
|
||||||
error = _('Provided value could not be converted to the specified unit')
|
|
||||||
|
|
||||||
if error:
|
|
||||||
if unit:
|
if unit:
|
||||||
error += f' ({unit})'
|
error += f' ({unit})'
|
||||||
|
|
||||||
|
except pint.errors.DimensionalityError:
|
||||||
|
error = _('Provided value could not be converted to the specified unit')
|
||||||
|
if unit:
|
||||||
|
error += f' ({unit})'
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error = _('Error') + ': ' + str(e)
|
||||||
|
|
||||||
|
if error:
|
||||||
raise ValidationError(error)
|
raise ValidationError(error)
|
||||||
|
|
||||||
# Return the converted value
|
# Calculate the "magnitude" of the value, as a float
|
||||||
return val
|
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause isuses
|
||||||
|
# So, we ensure that it is converted to a floating point value
|
||||||
|
# If we wish to return a "raw" value, some trickery is required
|
||||||
|
if unit:
|
||||||
|
magnitude = ureg.Quantity(val.to(unit)).magnitude
|
||||||
|
else:
|
||||||
|
magnitude = ureg.Quantity(val.to_base_units()).magnitude
|
||||||
|
|
||||||
|
magnitude = float(ureg.Quantity(magnitude).to_base_units().magnitude)
|
||||||
|
|
||||||
|
if strip_units:
|
||||||
|
return magnitude
|
||||||
|
elif unit or val.units:
|
||||||
|
return ureg.Quantity(magnitude, unit or val.units)
|
||||||
|
else:
|
||||||
|
return ureg.Quantity(magnitude)
|
||||||
|
|
||||||
|
|
||||||
|
def is_dimensionless(value):
|
||||||
|
"""Determine if the provided value is 'dimensionless'
|
||||||
|
|
||||||
|
A dimensionless value might look like:
|
||||||
|
|
||||||
|
0.1
|
||||||
|
1/2 dozen
|
||||||
|
three thousand
|
||||||
|
1.2 dozen
|
||||||
|
(etc)
|
||||||
|
"""
|
||||||
|
ureg = get_unit_registry()
|
||||||
|
|
||||||
|
# Ensure the provided value is in the right format
|
||||||
|
value = ureg.Quantity(value)
|
||||||
|
|
||||||
|
if value.units == ureg.dimensionless:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if value.to_base_units().units == ureg.dimensionless:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# At this point, the value is not dimensionless
|
||||||
|
return False
|
||||||
|
@ -42,6 +42,26 @@ from .validators import validate_overage
|
|||||||
class ConversionTest(TestCase):
|
class ConversionTest(TestCase):
|
||||||
"""Tests for conversion of physical units"""
|
"""Tests for conversion of physical units"""
|
||||||
|
|
||||||
|
def test_base_units(self):
|
||||||
|
"""Test conversion to specified base units"""
|
||||||
|
tests = {
|
||||||
|
"3": 3,
|
||||||
|
"3 dozen": 36,
|
||||||
|
"50 dozen kW": 600000,
|
||||||
|
"1 / 10": 0.1,
|
||||||
|
"1/2 kW": 500,
|
||||||
|
"1/2 dozen kW": 6000,
|
||||||
|
"0.005 MW": 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
for val, expected in tests.items():
|
||||||
|
q = InvenTree.conversion.convert_physical_value(val, 'W')
|
||||||
|
|
||||||
|
self.assertAlmostEqual(q, expected, 0.01)
|
||||||
|
|
||||||
|
q = InvenTree.conversion.convert_physical_value(val, 'W', strip_units=False)
|
||||||
|
self.assertAlmostEqual(float(q.magnitude), expected, 0.01)
|
||||||
|
|
||||||
def test_dimensionless_units(self):
|
def test_dimensionless_units(self):
|
||||||
"""Tests for 'dimensonless' unit quantities"""
|
"""Tests for 'dimensonless' unit quantities"""
|
||||||
|
|
||||||
@ -54,11 +74,21 @@ class ConversionTest(TestCase):
|
|||||||
'3 hundred': 300,
|
'3 hundred': 300,
|
||||||
'2 thousand': 2000,
|
'2 thousand': 2000,
|
||||||
'12 pieces': 12,
|
'12 pieces': 12,
|
||||||
|
'1 / 10': 0.1,
|
||||||
|
'1/2': 0.5,
|
||||||
|
'-1 / 16': -0.0625,
|
||||||
|
'3/2': 1.5,
|
||||||
|
'1/2 dozen': 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
for val, expected in tests.items():
|
for val, expected in tests.items():
|
||||||
q = InvenTree.conversion.convert_physical_value(val).to_base_units()
|
# Convert, and leave units
|
||||||
self.assertEqual(q.magnitude, expected)
|
q = InvenTree.conversion.convert_physical_value(val, strip_units=False)
|
||||||
|
self.assertAlmostEqual(float(q.magnitude), expected, 0.01)
|
||||||
|
|
||||||
|
# Convert, and strip units
|
||||||
|
q = InvenTree.conversion.convert_physical_value(val)
|
||||||
|
self.assertAlmostEqual(q, expected, 0.01)
|
||||||
|
|
||||||
def test_invalid_values(self):
|
def test_invalid_values(self):
|
||||||
"""Test conversion of invalid inputs"""
|
"""Test conversion of invalid inputs"""
|
||||||
@ -71,11 +101,19 @@ class ConversionTest(TestCase):
|
|||||||
'--',
|
'--',
|
||||||
'+',
|
'+',
|
||||||
'++',
|
'++',
|
||||||
|
'1/0',
|
||||||
|
'1/-',
|
||||||
]
|
]
|
||||||
|
|
||||||
for val in inputs:
|
for val in inputs:
|
||||||
|
# Test with a provided unit
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
InvenTree.conversion.convert_physical_value(val)
|
InvenTree.conversion.convert_physical_value(val, 'meter')
|
||||||
|
|
||||||
|
# Test dimensionless
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
result = InvenTree.conversion.convert_physical_value(val)
|
||||||
|
print("Testing invalid value:", val, result)
|
||||||
|
|
||||||
def test_custom_units(self):
|
def test_custom_units(self):
|
||||||
"""Tests for custom unit conversion"""
|
"""Tests for custom unit conversion"""
|
||||||
@ -104,8 +142,23 @@ class ConversionTest(TestCase):
|
|||||||
reg['hpmm']
|
reg['hpmm']
|
||||||
|
|
||||||
# Convert some values
|
# Convert some values
|
||||||
q = InvenTree.conversion.convert_physical_value('1 hpmm', 'henry / km')
|
tests = {
|
||||||
self.assertEqual(q.magnitude, 1000000)
|
'1': 1,
|
||||||
|
'1 hpmm': 1000000,
|
||||||
|
'1 / 10 hpmm': 100000,
|
||||||
|
'1 / 100 hpmm': 10000,
|
||||||
|
'0.3 hpmm': 300000,
|
||||||
|
'-7hpmm': -7000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
for val, expected in tests.items():
|
||||||
|
# Convert, and leave units
|
||||||
|
q = InvenTree.conversion.convert_physical_value(val, 'henry / km', strip_units=False)
|
||||||
|
self.assertAlmostEqual(float(q.magnitude), expected, 0.01)
|
||||||
|
|
||||||
|
# Convert and strip units
|
||||||
|
q = InvenTree.conversion.convert_physical_value(val, 'henry / km')
|
||||||
|
self.assertAlmostEqual(q, expected, 0.01)
|
||||||
|
|
||||||
|
|
||||||
class ValidatorTest(TestCase):
|
class ValidatorTest(TestCase):
|
||||||
|
@ -638,11 +638,12 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
|||||||
try:
|
try:
|
||||||
# Attempt conversion to specified unit
|
# Attempt conversion to specified unit
|
||||||
native_value = InvenTree.conversion.convert_physical_value(
|
native_value = InvenTree.conversion.convert_physical_value(
|
||||||
self.pack_quantity, self.part.units
|
self.pack_quantity, self.part.units,
|
||||||
|
strip_units=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# If part units are not provided, value must be dimensionless
|
# If part units are not provided, value must be dimensionless
|
||||||
if not self.part.units and native_value.units not in ['', 'dimensionless']:
|
if not self.part.units and not InvenTree.conversion.is_dimensionless(native_value):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'pack_quantity': _("Pack units must be compatible with the base part units")
|
'pack_quantity': _("Pack units must be compatible with the base part units")
|
||||||
})
|
})
|
||||||
|
@ -3576,8 +3576,7 @@ class PartParameter(MetadataMixin, models.Model):
|
|||||||
|
|
||||||
if self.template.units:
|
if self.template.units:
|
||||||
try:
|
try:
|
||||||
converted = InvenTree.conversion.convert_physical_value(self.data, self.template.units)
|
self.data_numeric = InvenTree.conversion.convert_physical_value(self.data, self.template.units)
|
||||||
self.data_numeric = float(converted.magnitude)
|
|
||||||
except (ValidationError, ValueError):
|
except (ValidationError, ValueError):
|
||||||
self.data_numeric = None
|
self.data_numeric = None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user