• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / InvenTree / 8535200664

03 Apr 2024 07:58AM UTC coverage: 91.742% (-0.8%) from 92.496%
8535200664

Pull #6855

github

web-flow
Merge f3149a282 into 6be2ede5e
Pull Request #6855: [PUI] Add licenses texts to PUI

8 of 22 new or added lines in 3 files covered. (36.36%)

174 existing lines in 20 files now uncovered.

30217 of 32937 relevant lines covered (91.74%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.69
/src/backend/InvenTree/InvenTree/conversion.py
1
"""Helper functions for converting between units."""
2

3
import logging
4
import re
5

6
from django.core.exceptions import ValidationError
7
from django.utils.translation import gettext_lazy as _
8

9
import pint
10

11
_unit_registry = None
12

13
logger = logging.getLogger('inventree')
14

15

16
def get_unit_registry():
17
    """Return a custom instance of the Pint UnitRegistry."""
18
    global _unit_registry
×
19

20
    # Cache the unit registry for speedier access
21
    if _unit_registry is None:
1✔
22
        return reload_unit_registry()
1✔
23
    return _unit_registry
1✔
24

25

26
def reload_unit_registry():
1✔
27
    """Reload the unit registry from the database.
28

29
    This function is called at startup, and whenever the database is updated.
30
    """
31
    import time
1✔
32

33
    t_start = time.time()
1✔
34

35
    global _unit_registry
×
36

37
    _unit_registry = None
1✔
38

39
    reg = pint.UnitRegistry(autoconvert_offset_to_baseunit=True)
1✔
40

41
    # Aliases for temperature units
42
    reg.define('@alias degC = Celsius')
1✔
43
    reg.define('@alias degF = Fahrenheit')
1✔
44
    reg.define('@alias degK = Kelvin')
1✔
45

46
    # Define some "standard" additional units
47
    reg.define('piece = 1')
1✔
48
    reg.define('each = 1 = ea')
1✔
49
    reg.define('dozen = 12 = dz')
1✔
50
    reg.define('hundred = 100')
1✔
51
    reg.define('thousand = 1000')
1✔
52

53
    # Allow for custom units to be defined in the database
54
    try:
1✔
55
        from common.models import CustomUnit
1✔
56

57
        for cu in CustomUnit.objects.all():
1✔
58
            try:
1✔
59
                reg.define(cu.fmt_string())
1✔
60
            except Exception as e:
×
61
                logger.exception(
62
                    'Failed to load custom unit: %s - %s', cu.fmt_string(), e
63
                )
64

65
        # Once custom units are loaded, save registry
66
        _unit_registry = reg
1✔
67

68
    except Exception:
1✔
69
        # Database is not ready, or CustomUnit model is not available
70
        pass
1✔
71

72
    dt = time.time() - t_start
1✔
73
    logger.debug('Loaded unit registry in %.3f s', dt)
1✔
74

75
    return reg
1✔
76

77

78
def from_engineering_notation(value):
1✔
79
    """Convert a provided value to 'natural' representation from 'engineering' notation.
80

81
    Ref: https://en.wikipedia.org/wiki/Engineering_notation
82

83
    In "engineering notation", the unit (or SI prefix) is often combined with the value,
84
    and replaces the decimal point.
85

86
    Examples:
87
    - 1K2 -> 1.2K
88
    - 3n05 -> 3.05n
89
    - 8R6 -> 8.6R
90

91
    And, we should also take into account any provided trailing strings:
92

93
    - 1K2 ohm -> 1.2K ohm
94
    - 10n005F -> 10.005nF
95
    """
96
    value = str(value).strip()
1✔
97

98
    pattern = '(\d+)([a-zA-Z]+)(\d+)(.*)'
1✔
99

100
    if match := re.match(pattern, value):
1✔
UNCOV
101
        left, prefix, right, suffix = match.groups()
×
UNCOV
102
        return f'{left}.{right}{prefix}{suffix}'
×
103

104
    return value
1✔
105

106

107
def convert_value(value, unit):
1✔
108
    """Attempt to convert a value to a specified unit.
109

110
    Arguments:
111
        value: The value to convert
112
        unit: The target unit to convert to
113

114
    Returns:
115
        The converted value (ideally a pint.Quantity value)
116

117
    Raises:
118
        Exception if the value cannot be converted to the specified unit
119
    """
120
    ureg = get_unit_registry()
1✔
121

122
    # Convert the provided value to a pint.Quantity object
123
    value = ureg.Quantity(value)
1✔
124

125
    # Convert to the specified unit
126
    if unit:
1✔
127
        if is_dimensionless(value):
1✔
128
            magnitude = value.to_base_units().magnitude
1✔
129
            value = ureg.Quantity(magnitude, unit)
1✔
130
        else:
×
131
            value = value.to(unit)
1✔
132

133
    return value
1✔
134

135

136
def convert_physical_value(value: str, unit: str = None, strip_units=True):
1✔
137
    """Validate that the provided value is a valid physical quantity.
138

139
    Arguments:
140
        value: Value to validate (str)
141
        unit: Optional unit to convert to, and validate against
142
        strip_units: If True, strip units from the returned value, and return only the dimension
143

144
    Raises:
145
        ValidationError: If the value is invalid or cannot be converted to the specified unit
146

147
    Returns:
148
        The converted quantity, in the specified units
149
    """
150
    ureg = get_unit_registry()
1✔
151

152
    # Check that the provided unit is available in the unit registry
153
    if unit:
1✔
154
        try:
1✔
155
            valid = unit in ureg
1✔
UNCOV
156
        except Exception as exc:
×
UNCOV
157
            valid = False
×
158

159
        if not valid:
1✔
UNCOV
160
            raise ValidationError(_(f'Invalid unit provided ({unit})'))
×
161

162
    original = str(value).strip()
1✔
163

164
    # Ensure that the value is a string
165
    value = str(value).strip() if value else ''
1✔
166
    unit = str(unit).strip() if unit else ''
1✔
167

168
    # Handle imperial length measurements
169
    if value.count("'") == 1 and value.endswith("'"):
1✔
UNCOV
170
        value = value.replace("'", ' feet')
×
171

172
    if value.count('"') == 1 and value.endswith('"'):
1✔
UNCOV
173
        value = value.replace('"', ' inches')
×
174

175
    # Error on blank values
176
    if not value:
1✔
177
        raise ValidationError(_('No value provided'))
×
178

179
    # Construct a list of values to "attempt" to convert
180
    attempts = [value]
1✔
181

182
    # Attempt to convert from engineering notation
183
    eng = from_engineering_notation(value)
1✔
184
    attempts.append(eng)
1✔
185

186
    # Append the unit, if provided
187
    # These are the "final" attempts to convert the value, and *must* appear after previous attempts
188
    if unit:
1✔
189
        attempts.append(f'{value}{unit}')
1✔
190
        attempts.append(f'{eng}{unit}')
1✔
191

192
    value = None
1✔
193

194
    # Run through the available "attempts", take the first successful result
195
    for attempt in attempts:
1✔
196
        try:
1✔
197
            value = convert_value(attempt, unit)
1✔
198
            break
1✔
199
        except Exception as exc:
1✔
200
            value = None
1✔
201

202
    if value is None:
1✔
203
        if unit:
1✔
204
            raise ValidationError(_(f'Could not convert {original} to {unit}'))
1✔
205
        else:
×
UNCOV
206
            raise ValidationError(_('Invalid quantity supplied'))
×
207

208
    # Calculate the "magnitude" of the value, as a float
209
    # If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
210
    # So, we ensure that it is converted to a floating point value
211
    # If we wish to return a "raw" value, some trickery is required
212
    try:
1✔
213
        if unit:
1✔
214
            magnitude = ureg.Quantity(value.to(ureg.Unit(unit))).magnitude
1✔
215
        else:
×
216
            magnitude = ureg.Quantity(value.to_base_units()).magnitude
1✔
217

218
        magnitude = float(ureg.Quantity(magnitude).to_base_units().magnitude)
1✔
219
    except Exception as exc:
×
220
        raise ValidationError(_(f'Invalid quantity supplied ({exc})'))
×
221

222
    if strip_units:
1✔
223
        return magnitude
1✔
224
    elif unit or value.units:
1✔
225
        return ureg.Quantity(magnitude, unit or value.units)
1✔
226
    return ureg.Quantity(magnitude)
×
227

228

229
def is_dimensionless(value):
1✔
230
    """Determine if the provided value is 'dimensionless'.
231

232
    A dimensionless value might look like:
233

234
    0.1
235
    1/2 dozen
236
    three thousand
237
    1.2 dozen
238
    (etc)
239
    """
240
    ureg = get_unit_registry()
1✔
241

242
    # Ensure the provided value is in the right format
243
    value = ureg.Quantity(value)
1✔
244

245
    if value.units == ureg.dimensionless:
1✔
246
        return True
1✔
247

248
    if value.to_base_units().units == ureg.dimensionless:
1✔
249
        return True
1✔
250

251
    # At this point, the value is not dimensionless
252
    return False
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc