Skip to content

Commit 5e5aefd

Browse files
authored
Merge branch 'master' into feature/multiplePartImage#t2
2 parents d9ce4a7 + b579ccd commit 5e5aefd

File tree

9 files changed

+200
-59
lines changed

9 files changed

+200
-59
lines changed

=0.9.4

Lines changed: 0 additions & 6 deletions
This file was deleted.

docs/docs/report/helpers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ Simple mathematical operators are available, as demonstrated in the example temp
267267

268268
### Input Types
269269

270-
These mathematical functions accept inputs of various input types, and attempt to perform the operation accordingly. Note that any inputs which are provided as strings will be converted to floating point numbers before the operation is performed.
270+
These mathematical functions accept inputs of various input types, and attempt to perform the operation accordingly. Note that any inputs which are provided as strings or numbers will be converted to `Decimal` class types before the operation is performed.
271271

272272
### add
273273

src/backend/InvenTree/InvenTree/helpers_model.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from urllib.parse import urljoin
77

88
from django.conf import settings
9+
from django.core.exceptions import ValidationError
910
from django.core.validators import URLValidator
1011
from django.db.utils import OperationalError, ProgrammingError
1112
from django.utils.translation import gettext_lazy as _
@@ -184,6 +185,7 @@ def render_currency(
184185
money: Money,
185186
decimal_places: Optional[int] = None,
186187
currency: Optional[str] = None,
188+
multiplier: Optional[Decimal] = None,
187189
min_decimal_places: Optional[int] = None,
188190
max_decimal_places: Optional[int] = None,
189191
include_symbol: bool = True,
@@ -194,6 +196,7 @@ def render_currency(
194196
money: The Money instance to be rendered
195197
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
196198
currency: Optionally convert to the specified currency
199+
multiplier: An optional multiplier to apply to the money amount before rendering
197200
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
198201
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
199202
include_symbol: If True, include the currency symbol in the output
@@ -202,7 +205,16 @@ def render_currency(
202205
return '-'
203206

204207
if type(money) is not Money:
205-
return '-'
208+
# Try to convert to a Money object
209+
try:
210+
money = Money(
211+
Decimal(str(money)),
212+
currency or get_global_setting('INVENTREE_DEFAULT_CURRENCY'),
213+
)
214+
except Exception:
215+
raise ValidationError(
216+
f"render_currency: {_('Invalid money value')}: '{money}' ({type(money).__name__})"
217+
)
206218

207219
if currency is not None:
208220
# Attempt to convert to the provided currency
@@ -212,6 +224,14 @@ def render_currency(
212224
except Exception:
213225
pass
214226

227+
if multiplier is not None:
228+
try:
229+
money *= Decimal(str(multiplier).strip())
230+
except Exception:
231+
raise ValidationError(
232+
f"render_currency: {_('Invalid multiplier value')}: '{multiplier}' ({type(multiplier).__name__})"
233+
)
234+
215235
if min_decimal_places is None or not isinstance(min_decimal_places, (int, float)):
216236
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)
217237

src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@ def str2bool(x, *args, **kwargs):
7070
return InvenTree.helpers.str2bool(x)
7171

7272

73-
@register.simple_tag()
74-
def add(x, y, *args, **kwargs):
75-
"""Add two numbers together."""
76-
return x + y
77-
78-
7973
@register.simple_tag()
8074
def to_list(*args):
8175
"""Return the input arguments as list."""

src/backend/InvenTree/part/test_part.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ def test_str2bool(self):
3939
self.assertEqual(int(inventree_extras.str2bool('none')), False)
4040
self.assertEqual(int(inventree_extras.str2bool('off')), False)
4141

42-
def test_add(self):
43-
"""Test that the 'add."""
44-
self.assertEqual(int(inventree_extras.add(3, 5)), 8)
45-
4642
def test_inventree_instance_name(self):
4743
"""Test the 'instance name' setting."""
4844
self.assertEqual(inventree_extras.inventree_instance_name(), 'InvenTree')

src/backend/InvenTree/report/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,9 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput:
546546
msg = _('Template syntax error')
547547
output.mark_failure(msg)
548548
raise ValidationError(f'{msg}: {e!s}')
549+
except ValidationError as e:
550+
output.mark_failure(str(e))
551+
raise e
549552
except Exception as e:
550553
msg = _('Error rendering report')
551554
output.mark_failure(msg)
@@ -582,6 +585,9 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput:
582585
msg = _('Template syntax error')
583586
output.mark_failure(error=_('Template syntax error'))
584587
raise ValidationError(f'{msg}: {e!s}')
588+
except ValidationError as e:
589+
output.mark_failure(str(e))
590+
raise e
585591
except Exception as e:
586592
msg = _('Error rendering report')
587593
output.mark_failure(error=msg)

src/backend/InvenTree/report/templatetags/report.py

Lines changed: 133 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
import os
66
from datetime import date, datetime
7-
from decimal import Decimal
7+
from decimal import Decimal, InvalidOperation
88
from typing import Any, Optional, Union
99

1010
from django import template
@@ -413,54 +413,161 @@ def internal_link(link, text) -> str:
413413
return mark_safe(f'<a href="{url}">{text}</a>')
414414

415415

416-
def destringify(value: Any) -> Any:
417-
"""Convert a string value into a float.
416+
def make_decimal(value: Any) -> Any:
417+
"""Convert an input value into a Decimal.
418418
419-
- If the value is a string, attempt to convert it to a float.
420-
- If conversion fails, return the original string.
421-
- If the value is not a string, return it unchanged.
419+
- Converts [string, int, float] types into Decimal
420+
- If conversion fails, returns the original value
422421
423422
The purpose of this function is to provide "seamless" math operations in templates,
424423
where numeric values may be provided as strings, or converted to strings during template rendering.
425424
"""
426-
if isinstance(value, str):
427-
value = value.strip()
425+
if any(isinstance(value, t) for t in [int, float, str]):
428426
try:
429-
return float(value)
430-
except ValueError:
431-
return value
427+
value = Decimal(str(value).strip())
428+
except (InvalidOperation, TypeError, ValueError):
429+
logger.warning(
430+
'make_decimal: Failed to convert value to Decimal: %s (%s)',
431+
value,
432+
type(value),
433+
)
432434

433435
return value
434436

435437

438+
def cast_to_type(value: Any, cast: type) -> Any:
439+
"""Attempt to cast a value to the provided type.
440+
441+
If casting fails, the original value is returned.
442+
"""
443+
if cast is not None:
444+
try:
445+
value = cast(value)
446+
except (ValueError, TypeError):
447+
pass
448+
449+
return value
450+
451+
452+
def debug_vars(x: Any, y: Any) -> str:
453+
"""Return a debug string showing the types and values of two variables."""
454+
return f": x='{x}' ({type(x).__name__}), y='{y}' ({type(y).__name__})"
455+
456+
436457
@register.simple_tag()
437-
def add(x: Any, y: Any) -> Any:
438-
"""Add two numbers (or number like values) together."""
439-
return destringify(x) + destringify(y)
458+
def add(x: Any, y: Any, cast: Optional[type] = None) -> Any:
459+
"""Add two numbers (or number like values) together.
460+
461+
Arguments:
462+
x: The first value to add
463+
y: The second value to add
464+
cast: Optional type to cast the result to (e.g. int, float, str)
465+
466+
Raises:
467+
ValidationError: If the values cannot be added together
468+
"""
469+
try:
470+
result = make_decimal(x) + make_decimal(y)
471+
except (InvalidOperation, TypeError, ValueError):
472+
raise ValidationError(
473+
_('Cannot add values of incompatible types') + debug_vars(x, y)
474+
)
475+
return cast_to_type(result, cast)
440476

441477

442478
@register.simple_tag()
443-
def subtract(x: Any, y: Any) -> Any:
444-
"""Subtract one number (or number-like value) from another."""
445-
return destringify(x) - destringify(y)
479+
def subtract(x: Any, y: Any, cast: Optional[type] = None) -> Any:
480+
"""Subtract one number (or number-like value) from another.
481+
482+
Arguments:
483+
x: The value to be subtracted from
484+
y: The value to be subtracted
485+
cast: Optional type to cast the result to (e.g. int, float, str)
486+
487+
Raises:
488+
ValidationError: If the values cannot be subtracted
489+
"""
490+
try:
491+
result = make_decimal(x) - make_decimal(y)
492+
except (InvalidOperation, TypeError, ValueError):
493+
raise ValidationError(
494+
_('Cannot subtract values of incompatible types') + debug_vars(x, y)
495+
)
496+
497+
return cast_to_type(result, cast)
446498

447499

448500
@register.simple_tag()
449-
def multiply(x: Any, y: Any) -> Any:
450-
"""Multiply two numbers (or number-like values) together."""
451-
return destringify(x) * destringify(y)
501+
def multiply(x: Any, y: Any, cast: Optional[type] = None) -> Any:
502+
"""Multiply two numbers (or number-like values) together.
503+
504+
Arguments:
505+
x: The first value to multiply
506+
y: The second value to multiply
507+
cast: Optional type to cast the result to (e.g. int, float, str)
508+
509+
Raises:
510+
ValidationError: If the values cannot be multiplied together
511+
"""
512+
try:
513+
result = make_decimal(x) * make_decimal(y)
514+
except (InvalidOperation, TypeError, ValueError):
515+
raise ValidationError(
516+
_('Cannot multiply values of incompatible types') + debug_vars(x, y)
517+
)
518+
519+
return cast_to_type(result, cast)
452520

453521

454522
@register.simple_tag()
455-
def divide(x: Any, y: Any) -> Any:
456-
"""Divide one number (or number-like value) by another."""
457-
return destringify(x) / destringify(y)
523+
def divide(x: Any, y: Any, cast: Optional[type] = None) -> Any:
524+
"""Divide one number (or number-like value) by another.
525+
526+
Arguments:
527+
x: The value to be divided
528+
y: The value to divide by
529+
cast: Optional type to cast the result to (e.g. int, float, str)
530+
531+
Raises:
532+
ValidationError: If the values cannot be divided
533+
"""
534+
try:
535+
result = make_decimal(x) / make_decimal(y)
536+
except (InvalidOperation, TypeError, ValueError):
537+
raise ValidationError(
538+
_('Cannot divide values of incompatible types') + debug_vars(x, y)
539+
)
540+
except ZeroDivisionError:
541+
raise ValidationError(_('Cannot divide by zero') + debug_vars(x, y))
542+
543+
return cast_to_type(result, cast)
458544

459545

460546
@register.simple_tag()
461-
def modulo(x: Any, y: Any) -> Any:
462-
"""Calculate the modulo of one number (or number-like value) by another."""
463-
return destringify(x) % destringify(y)
547+
def modulo(x: Any, y: Any, cast: Optional[type] = None) -> Any:
548+
"""Calculate the modulo of one number (or number-like value) by another.
549+
550+
Arguments:
551+
x: The first value to be used in the modulo operation
552+
y: The second value to be used in the modulo operation
553+
cast: Optional type to cast the result to (e.g. int, float, str)
554+
555+
Raises:
556+
ValidationError: If the values cannot be used in a modulo operation
557+
"""
558+
try:
559+
result = make_decimal(x) % make_decimal(y)
560+
except (InvalidOperation, TypeError, ValueError):
561+
raise ValidationError(
562+
_('Cannot perform modulo operation with values of incompatible types')
563+
+ debug_vars(x, y)
564+
)
565+
except ZeroDivisionError:
566+
raise ValidationError(
567+
_('Cannot perform modulo operation with divisor of zero') + debug_vars(x, y)
568+
)
569+
570+
return cast_to_type(result, cast)
464571

465572

466573
@register.simple_tag

0 commit comments

Comments
 (0)