mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Improve unit conversion (#5658)
- In previous implementation, could not have a value "10k" for a unit of "m" - Now, correct result is obtained (10k [m] = 10[km]) - Still passes all previous unit tests - Simpler code, too
This commit is contained in:
parent
c18bc4e1d1
commit
ffda700244
@ -85,65 +85,66 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
The converted quantity, in the specified units
|
||||
"""
|
||||
|
||||
original = str(value).strip()
|
||||
|
||||
# Ensure that the value is a string
|
||||
value = str(value).strip()
|
||||
value = str(value).strip() if value else ''
|
||||
unit = str(unit).strip() if unit else ''
|
||||
|
||||
# Error on blank values
|
||||
if not value:
|
||||
raise ValidationError(_('No value provided'))
|
||||
|
||||
# Create a "backup" value which be tried if the first value fails
|
||||
# e.g. value = "10k" and unit = "ohm" -> "10kohm"
|
||||
# e.g. value = "10m" and unit = "F" -> "10mF"
|
||||
if unit:
|
||||
backup_value = value + unit
|
||||
else:
|
||||
backup_value = None
|
||||
|
||||
ureg = get_unit_registry()
|
||||
error = ''
|
||||
|
||||
try:
|
||||
# Convert to a quantity
|
||||
val = ureg.Quantity(value)
|
||||
value = ureg.Quantity(value)
|
||||
|
||||
if unit:
|
||||
|
||||
if is_dimensionless(val):
|
||||
# If the provided value is dimensionless, assume that the unit is correct
|
||||
val = ureg.Quantity(value, unit)
|
||||
if is_dimensionless(value):
|
||||
magnitude = value.to_base_units().magnitude
|
||||
value = ureg.Quantity(magnitude, unit)
|
||||
else:
|
||||
# Convert to the provided unit (may raise an exception)
|
||||
val = val.to(ureg.Unit(unit))
|
||||
value = value.to(unit)
|
||||
|
||||
# At this point we *should* have a valid pint value
|
||||
# To double check, look at the maginitude
|
||||
float(ureg.Quantity(val.magnitude).magnitude)
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
error = _('Provided value is not a valid number')
|
||||
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
|
||||
error = _('Provided value has an invalid unit')
|
||||
except Exception:
|
||||
if backup_value:
|
||||
try:
|
||||
value = ureg.Quantity(backup_value)
|
||||
except Exception:
|
||||
value = None
|
||||
else:
|
||||
value = None
|
||||
|
||||
if value is None:
|
||||
if 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(_(f'Could not convert {original} to {unit}'))
|
||||
else:
|
||||
raise ValidationError(_("Invalid quantity supplied"))
|
||||
|
||||
# Calculate the "magnitude" of the value, as a float
|
||||
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause isuses
|
||||
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
|
||||
# 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(ureg.Unit(unit))).magnitude
|
||||
magnitude = ureg.Quantity(value.to(ureg.Unit(unit))).magnitude
|
||||
else:
|
||||
magnitude = ureg.Quantity(val.to_base_units()).magnitude
|
||||
magnitude = ureg.Quantity(value.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)
|
||||
elif unit or value.units:
|
||||
return ureg.Quantity(magnitude, unit or value.units)
|
||||
else:
|
||||
return ureg.Quantity(magnitude)
|
||||
|
||||
|
@ -42,6 +42,22 @@ from .validators import validate_overage
|
||||
class ConversionTest(TestCase):
|
||||
"""Tests for conversion of physical units"""
|
||||
|
||||
def test_prefixes(self):
|
||||
"""Test inputs where prefixes are used"""
|
||||
|
||||
tests = {
|
||||
"3": 3,
|
||||
"3m": 3,
|
||||
"3mm": 0.003,
|
||||
"3k": 3000,
|
||||
"3u": 0.000003,
|
||||
"3 inch": 0.0762,
|
||||
}
|
||||
|
||||
for val, expected in tests.items():
|
||||
q = InvenTree.conversion.convert_physical_value(val, 'm')
|
||||
self.assertAlmostEqual(q, expected, 3)
|
||||
|
||||
def test_base_units(self):
|
||||
"""Test conversion to specified base units"""
|
||||
tests = {
|
||||
@ -56,14 +72,12 @@ class ConversionTest(TestCase):
|
||||
|
||||
for val, expected in tests.items():
|
||||
q = InvenTree.conversion.convert_physical_value(val, 'W')
|
||||
|
||||
self.assertAlmostEqual(q, expected, 0.01)
|
||||
|
||||
self.assertAlmostEqual(q, expected, places=2)
|
||||
q = InvenTree.conversion.convert_physical_value(val, 'W', strip_units=False)
|
||||
self.assertAlmostEqual(float(q.magnitude), expected, 0.01)
|
||||
self.assertAlmostEqual(float(q.magnitude), expected, places=2)
|
||||
|
||||
def test_dimensionless_units(self):
|
||||
"""Tests for 'dimensonless' unit quantities"""
|
||||
"""Tests for 'dimensionless' unit quantities"""
|
||||
|
||||
# Test some dimensionless units
|
||||
tests = {
|
||||
@ -84,25 +98,20 @@ class ConversionTest(TestCase):
|
||||
for val, expected in tests.items():
|
||||
# Convert, and leave units
|
||||
q = InvenTree.conversion.convert_physical_value(val, strip_units=False)
|
||||
self.assertAlmostEqual(float(q.magnitude), expected, 0.01)
|
||||
self.assertAlmostEqual(float(q.magnitude), expected, 3)
|
||||
|
||||
# Convert, and strip units
|
||||
q = InvenTree.conversion.convert_physical_value(val)
|
||||
self.assertAlmostEqual(q, expected, 0.01)
|
||||
self.assertAlmostEqual(q, expected, 3)
|
||||
|
||||
def test_invalid_values(self):
|
||||
"""Test conversion of invalid inputs"""
|
||||
|
||||
inputs = [
|
||||
'-',
|
||||
';;',
|
||||
'-x',
|
||||
'?',
|
||||
'--',
|
||||
'+',
|
||||
'++',
|
||||
'1/0',
|
||||
'1/-',
|
||||
'xyz',
|
||||
'12B45C'
|
||||
]
|
||||
|
||||
for val in inputs:
|
||||
@ -112,8 +121,7 @@ class ConversionTest(TestCase):
|
||||
|
||||
# Test dimensionless
|
||||
with self.assertRaises(ValidationError):
|
||||
result = InvenTree.conversion.convert_physical_value(val)
|
||||
print("Testing invalid value:", val, result)
|
||||
InvenTree.conversion.convert_physical_value(val)
|
||||
|
||||
def test_custom_units(self):
|
||||
"""Tests for custom unit conversion"""
|
||||
@ -154,11 +162,11 @@ class ConversionTest(TestCase):
|
||||
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)
|
||||
self.assertAlmostEqual(float(q.magnitude), expected, 2)
|
||||
|
||||
# Convert and strip units
|
||||
q = InvenTree.conversion.convert_physical_value(val, 'henry / km')
|
||||
self.assertAlmostEqual(q, expected, 0.01)
|
||||
self.assertAlmostEqual(q, expected, 2)
|
||||
|
||||
|
||||
class ValidatorTest(TestCase):
|
||||
@ -857,7 +865,7 @@ class CurrencyTests(TestCase):
|
||||
class TestStatus(TestCase):
|
||||
"""Unit tests for status functions."""
|
||||
|
||||
def test_check_system_healt(self):
|
||||
def test_check_system_health(self):
|
||||
"""Test that the system health check is false in testing -> background worker not running."""
|
||||
self.assertEqual(status.check_system_health(), False)
|
||||
|
||||
@ -953,7 +961,7 @@ class TestSettings(InvenTreeTestCase):
|
||||
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
||||
registry.reload_plugins(full_reload=True)
|
||||
|
||||
# Check that there was anotehr run
|
||||
# Check that there was another run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, True)
|
||||
|
||||
@ -1147,7 +1155,7 @@ class BarcodeMixinTest(InvenTreeTestCase):
|
||||
self.assertEqual(StockItem.barcode_model_type(), 'stockitem')
|
||||
self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
|
||||
|
||||
def test_bacode_hash(self):
|
||||
def test_barcode_hash(self):
|
||||
"""Test that the barcode hashing function provides correct results"""
|
||||
|
||||
# Test multiple values for the hashing function
|
||||
@ -1176,7 +1184,7 @@ class SanitizerTest(TestCase):
|
||||
# Test that valid string
|
||||
self.assertEqual(valid_string, sanitize_svg(valid_string))
|
||||
|
||||
# Test that invalid string is cleanded
|
||||
# Test that invalid string is cleaned
|
||||
self.assertNotEqual(dangerous_string, sanitize_svg(dangerous_string))
|
||||
|
||||
|
||||
|
@ -484,7 +484,7 @@ class PluginsRegistry:
|
||||
t_start = time.time()
|
||||
plg_i: InvenTreePlugin = plg()
|
||||
dt = time.time() - t_start
|
||||
logger.info('Loaded plugin `%s` in %.3fs', plg_name, dt)
|
||||
logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt)
|
||||
except Exception as error:
|
||||
handle_error(error, log_name='init') # log error and raise it -> disable plugin
|
||||
logger.warning("Plugin `%s` could not be loaded", plg_name)
|
||||
@ -554,7 +554,7 @@ class PluginsRegistry:
|
||||
if hasattr(mixin, '_deactivate_mixin'):
|
||||
mixin._deactivate_mixin(self, force_reload=force_reload)
|
||||
|
||||
logger.info('Done deactivating')
|
||||
logger.debug('Finished deactivating plugins')
|
||||
# endregion
|
||||
|
||||
# region mixin specific loading ...
|
||||
|
@ -257,6 +257,10 @@ function showApiError(xhr, url) {
|
||||
title = '{% trans "Error 408: Timeout" %}';
|
||||
message = '{% trans "Connection timeout while requesting data from server" %}';
|
||||
break;
|
||||
case 503:
|
||||
title = '{% trans "Error 503: Service Unavailable" %}';
|
||||
message = '{% trans "The server is currently unavailable" %}';
|
||||
break;
|
||||
default:
|
||||
title = '{% trans "Unhandled Error Code" %}';
|
||||
message = `{% trans "Error code" %}: ${xhr.status}`;
|
||||
|
Loading…
Reference in New Issue
Block a user