mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-29 20:30:39 +00:00
Report tag fixes (#10668)
* remove duplicate template tag * Add "multiplier" argument to render_currency * Improve render_currency - Enable conversion of non-money values to a Money instance * Improve maths tags - Convert values to Decimal - Ability to cast result to different type * Updated docs * Improved feedback from maths tags * Updated unit testing * Improved rendering of printing errors * Add extra test for render_currency tag * Enfoce multiplier type * Fix docstrings * Improved error handling * Remove defunct unit test * Fix unit tests
This commit is contained in:
@@ -267,7 +267,7 @@ Simple mathematical operators are available, as demonstrated in the example temp
|
|||||||
|
|
||||||
### Input Types
|
### Input Types
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### add
|
### add
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Optional, cast
|
|||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -184,6 +185,7 @@ def render_currency(
|
|||||||
money: Money,
|
money: Money,
|
||||||
decimal_places: Optional[int] = None,
|
decimal_places: Optional[int] = None,
|
||||||
currency: Optional[str] = None,
|
currency: Optional[str] = None,
|
||||||
|
multiplier: Optional[Decimal] = None,
|
||||||
min_decimal_places: Optional[int] = None,
|
min_decimal_places: Optional[int] = None,
|
||||||
max_decimal_places: Optional[int] = None,
|
max_decimal_places: Optional[int] = None,
|
||||||
include_symbol: bool = True,
|
include_symbol: bool = True,
|
||||||
@@ -194,6 +196,7 @@ def render_currency(
|
|||||||
money: The Money instance to be rendered
|
money: The Money instance to be rendered
|
||||||
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||||
currency: Optionally convert to the specified currency
|
currency: Optionally convert to the specified currency
|
||||||
|
multiplier: An optional multiplier to apply to the money amount before rendering
|
||||||
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
||||||
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||||
include_symbol: If True, include the currency symbol in the output
|
include_symbol: If True, include the currency symbol in the output
|
||||||
@@ -202,7 +205,16 @@ def render_currency(
|
|||||||
return '-'
|
return '-'
|
||||||
|
|
||||||
if type(money) is not Money:
|
if type(money) is not Money:
|
||||||
return '-'
|
# Try to convert to a Money object
|
||||||
|
try:
|
||||||
|
money = Money(
|
||||||
|
Decimal(str(money)),
|
||||||
|
currency or get_global_setting('INVENTREE_DEFAULT_CURRENCY'),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError(
|
||||||
|
f"render_currency: {_('Invalid money value')}: '{money}' ({type(money).__name__})"
|
||||||
|
)
|
||||||
|
|
||||||
if currency is not None:
|
if currency is not None:
|
||||||
# Attempt to convert to the provided currency
|
# Attempt to convert to the provided currency
|
||||||
@@ -212,6 +224,14 @@ def render_currency(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if multiplier is not None:
|
||||||
|
try:
|
||||||
|
money *= Decimal(str(multiplier).strip())
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError(
|
||||||
|
f"render_currency: {_('Invalid multiplier value')}: '{multiplier}' ({type(multiplier).__name__})"
|
||||||
|
)
|
||||||
|
|
||||||
if min_decimal_places is None or not isinstance(min_decimal_places, (int, float)):
|
if min_decimal_places is None or not isinstance(min_decimal_places, (int, float)):
|
||||||
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||||
|
|
||||||
|
|||||||
@@ -70,12 +70,6 @@ def str2bool(x, *args, **kwargs):
|
|||||||
return InvenTree.helpers.str2bool(x)
|
return InvenTree.helpers.str2bool(x)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
|
||||||
def add(x, y, *args, **kwargs):
|
|
||||||
"""Add two numbers together."""
|
|
||||||
return x + y
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def to_list(*args):
|
def to_list(*args):
|
||||||
"""Return the input arguments as list."""
|
"""Return the input arguments as list."""
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ class TemplateTagTest(InvenTreeTestCase):
|
|||||||
self.assertEqual(int(inventree_extras.str2bool('none')), False)
|
self.assertEqual(int(inventree_extras.str2bool('none')), False)
|
||||||
self.assertEqual(int(inventree_extras.str2bool('off')), False)
|
self.assertEqual(int(inventree_extras.str2bool('off')), False)
|
||||||
|
|
||||||
def test_add(self):
|
|
||||||
"""Test that the 'add."""
|
|
||||||
self.assertEqual(int(inventree_extras.add(3, 5)), 8)
|
|
||||||
|
|
||||||
def test_inventree_instance_name(self):
|
def test_inventree_instance_name(self):
|
||||||
"""Test the 'instance name' setting."""
|
"""Test the 'instance name' setting."""
|
||||||
self.assertEqual(inventree_extras.inventree_instance_name(), 'InvenTree')
|
self.assertEqual(inventree_extras.inventree_instance_name(), 'InvenTree')
|
||||||
|
|||||||
@@ -546,6 +546,9 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
msg = _('Template syntax error')
|
msg = _('Template syntax error')
|
||||||
output.mark_failure(msg)
|
output.mark_failure(msg)
|
||||||
raise ValidationError(f'{msg}: {e!s}')
|
raise ValidationError(f'{msg}: {e!s}')
|
||||||
|
except ValidationError as e:
|
||||||
|
output.mark_failure(str(e))
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = _('Error rendering report')
|
msg = _('Error rendering report')
|
||||||
output.mark_failure(msg)
|
output.mark_failure(msg)
|
||||||
@@ -582,6 +585,9 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
msg = _('Template syntax error')
|
msg = _('Template syntax error')
|
||||||
output.mark_failure(error=_('Template syntax error'))
|
output.mark_failure(error=_('Template syntax error'))
|
||||||
raise ValidationError(f'{msg}: {e!s}')
|
raise ValidationError(f'{msg}: {e!s}')
|
||||||
|
except ValidationError as e:
|
||||||
|
output.mark_failure(str(e))
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = _('Error rendering report')
|
msg = _('Error rendering report')
|
||||||
output.mark_failure(error=msg)
|
output.mark_failure(error=msg)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import base64
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal, InvalidOperation
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
@@ -413,54 +413,161 @@ def internal_link(link, text) -> str:
|
|||||||
return mark_safe(f'<a href="{url}">{text}</a>')
|
return mark_safe(f'<a href="{url}">{text}</a>')
|
||||||
|
|
||||||
|
|
||||||
def destringify(value: Any) -> Any:
|
def make_decimal(value: Any) -> Any:
|
||||||
"""Convert a string value into a float.
|
"""Convert an input value into a Decimal.
|
||||||
|
|
||||||
- If the value is a string, attempt to convert it to a float.
|
- Converts [string, int, float] types into Decimal
|
||||||
- If conversion fails, return the original string.
|
- If conversion fails, returns the original value
|
||||||
- If the value is not a string, return it unchanged.
|
|
||||||
|
|
||||||
The purpose of this function is to provide "seamless" math operations in templates,
|
The purpose of this function is to provide "seamless" math operations in templates,
|
||||||
where numeric values may be provided as strings, or converted to strings during template rendering.
|
where numeric values may be provided as strings, or converted to strings during template rendering.
|
||||||
"""
|
"""
|
||||||
if isinstance(value, str):
|
if any(isinstance(value, t) for t in [int, float, str]):
|
||||||
value = value.strip()
|
|
||||||
try:
|
try:
|
||||||
return float(value)
|
value = Decimal(str(value).strip())
|
||||||
except ValueError:
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
return value
|
logger.warning(
|
||||||
|
'make_decimal: Failed to convert value to Decimal: %s (%s)',
|
||||||
|
value,
|
||||||
|
type(value),
|
||||||
|
)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
def cast_to_type(value: Any, cast: type) -> Any:
|
||||||
def add(x: Any, y: Any) -> Any:
|
"""Attempt to cast a value to the provided type.
|
||||||
"""Add two numbers (or number like values) together."""
|
|
||||||
return destringify(x) + destringify(y)
|
If casting fails, the original value is returned.
|
||||||
|
"""
|
||||||
|
if cast is not None:
|
||||||
|
try:
|
||||||
|
value = cast(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def debug_vars(x: Any, y: Any) -> str:
|
||||||
|
"""Return a debug string showing the types and values of two variables."""
|
||||||
|
return f": x='{x}' ({type(x).__name__}), y='{y}' ({type(y).__name__})"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def subtract(x: Any, y: Any) -> Any:
|
def add(x: Any, y: Any, cast: Optional[type] = None) -> Any:
|
||||||
"""Subtract one number (or number-like value) from another."""
|
"""Add two numbers (or number like values) together.
|
||||||
return destringify(x) - destringify(y)
|
|
||||||
|
Arguments:
|
||||||
|
x: The first value to add
|
||||||
|
y: The second value to add
|
||||||
|
cast: Optional type to cast the result to (e.g. int, float, str)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the values cannot be added together
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = make_decimal(x) + make_decimal(y)
|
||||||
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Cannot add values of incompatible types') + debug_vars(x, y)
|
||||||
|
)
|
||||||
|
return cast_to_type(result, cast)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def multiply(x: Any, y: Any) -> Any:
|
def subtract(x: Any, y: Any, cast: Optional[type] = None) -> Any:
|
||||||
"""Multiply two numbers (or number-like values) together."""
|
"""Subtract one number (or number-like value) from another.
|
||||||
return destringify(x) * destringify(y)
|
|
||||||
|
Arguments:
|
||||||
|
x: The value to be subtracted from
|
||||||
|
y: The value to be subtracted
|
||||||
|
cast: Optional type to cast the result to (e.g. int, float, str)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the values cannot be subtracted
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = make_decimal(x) - make_decimal(y)
|
||||||
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Cannot subtract values of incompatible types') + debug_vars(x, y)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cast_to_type(result, cast)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def divide(x: Any, y: Any) -> Any:
|
def multiply(x: Any, y: Any, cast: Optional[type] = None) -> Any:
|
||||||
"""Divide one number (or number-like value) by another."""
|
"""Multiply two numbers (or number-like values) together.
|
||||||
return destringify(x) / destringify(y)
|
|
||||||
|
Arguments:
|
||||||
|
x: The first value to multiply
|
||||||
|
y: The second value to multiply
|
||||||
|
cast: Optional type to cast the result to (e.g. int, float, str)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the values cannot be multiplied together
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = make_decimal(x) * make_decimal(y)
|
||||||
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Cannot multiply values of incompatible types') + debug_vars(x, y)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cast_to_type(result, cast)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def modulo(x: Any, y: Any) -> Any:
|
def divide(x: Any, y: Any, cast: Optional[type] = None) -> Any:
|
||||||
"""Calculate the modulo of one number (or number-like value) by another."""
|
"""Divide one number (or number-like value) by another.
|
||||||
return destringify(x) % destringify(y)
|
|
||||||
|
Arguments:
|
||||||
|
x: The value to be divided
|
||||||
|
y: The value to divide by
|
||||||
|
cast: Optional type to cast the result to (e.g. int, float, str)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the values cannot be divided
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = make_decimal(x) / make_decimal(y)
|
||||||
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Cannot divide values of incompatible types') + debug_vars(x, y)
|
||||||
|
)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
raise ValidationError(_('Cannot divide by zero') + debug_vars(x, y))
|
||||||
|
|
||||||
|
return cast_to_type(result, cast)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def modulo(x: Any, y: Any, cast: Optional[type] = None) -> Any:
|
||||||
|
"""Calculate the modulo of one number (or number-like value) by another.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
x: The first value to be used in the modulo operation
|
||||||
|
y: The second value to be used in the modulo operation
|
||||||
|
cast: Optional type to cast the result to (e.g. int, float, str)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the values cannot be used in a modulo operation
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = make_decimal(x) % make_decimal(y)
|
||||||
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Cannot perform modulo operation with values of incompatible types')
|
||||||
|
+ debug_vars(x, y)
|
||||||
|
)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
raise ValidationError(
|
||||||
|
_('Cannot perform modulo operation with divisor of zero') + debug_vars(x, y)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cast_to_type(result, cast)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Test for custom report tags."""
|
"""Test for custom report tags."""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -198,23 +199,27 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
"""Simple tests for mathematical operator tags."""
|
"""Simple tests for mathematical operator tags."""
|
||||||
self.assertEqual(report_tags.add(1, 2), 3)
|
self.assertEqual(report_tags.add(1, 2), 3)
|
||||||
self.assertEqual(report_tags.add('-33', '33'), 0)
|
self.assertEqual(report_tags.add('-33', '33'), 0)
|
||||||
self.assertEqual(report_tags.subtract(10, 4.2), 5.8)
|
self.assertEqual(report_tags.add(4.5, Decimal(4.5), cast=float), 9.0)
|
||||||
self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
|
self.assertEqual(report_tags.subtract(10, 4.2, cast=float), 5.8)
|
||||||
self.assertEqual(report_tags.multiply('-2', 4), -8.0)
|
self.assertEqual(report_tags.multiply(2.3, 4, cast=str), '9.2')
|
||||||
|
self.assertEqual(report_tags.multiply('-2', 4), -8)
|
||||||
self.assertEqual(report_tags.divide(100, 5), 20)
|
self.assertEqual(report_tags.divide(100, 5), 20)
|
||||||
|
|
||||||
self.assertEqual(report_tags.modulo(10, 3), 1)
|
self.assertEqual(report_tags.modulo(10, 3), 1)
|
||||||
self.assertEqual(report_tags.modulo('10', '4'), 2)
|
self.assertEqual(report_tags.modulo('10', '4'), 2)
|
||||||
|
|
||||||
with self.assertRaises(ZeroDivisionError):
|
with self.assertRaises(ValidationError):
|
||||||
report_tags.divide(100, 0)
|
report_tags.divide(100, 0)
|
||||||
|
|
||||||
def test_maths_tags_with_strings(self):
|
def test_maths_tags_with_strings(self):
|
||||||
"""Tests for mathematical operator tags with string inputs."""
|
"""Tests for mathematical operator tags with string inputs."""
|
||||||
self.assertEqual(report_tags.add('10', '20'), 30)
|
self.assertEqual(report_tags.add(Decimal('10'), '20'), 30)
|
||||||
self.assertEqual(report_tags.subtract('50.5', '20.2'), 30.3)
|
self.assertEqual(report_tags.subtract('50.5', '20.2'), Decimal('30.3'))
|
||||||
self.assertEqual(report_tags.multiply(3.0000000000000, '7'), 21)
|
self.assertEqual(report_tags.multiply(3.0000000000000, '7'), 21)
|
||||||
self.assertEqual(report_tags.divide('100.0', '4'), 25.0)
|
|
||||||
|
result = report_tags.divide(100, Decimal('4'), cast=int)
|
||||||
|
self.assertEqual(result, 25)
|
||||||
|
self.assertIsInstance(result, int)
|
||||||
|
|
||||||
def test_maths_tags_with_decimal(self):
|
def test_maths_tags_with_decimal(self):
|
||||||
"""Tests for mathematical operator tags with Decimal inputs."""
|
"""Tests for mathematical operator tags with Decimal inputs."""
|
||||||
@@ -231,6 +236,8 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
report_tags.divide(Decimal('10.0'), Decimal('2.000')), Decimal('5.0')
|
report_tags.divide(Decimal('10.0'), Decimal('2.000')), Decimal('5.0')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.assertEqual(report_tags.multiply(3.3, Decimal('2.0')), Decimal('6.6'))
|
||||||
|
|
||||||
def test_maths_tags_with_money(self):
|
def test_maths_tags_with_money(self):
|
||||||
"""Tests for mathematical operator tags with Money inputs."""
|
"""Tests for mathematical operator tags with Money inputs."""
|
||||||
m1 = Money(100, 'USD')
|
m1 = Money(100, 'USD')
|
||||||
@@ -239,20 +246,24 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
self.assertEqual(report_tags.add(m1, m2), Money(150, 'USD'))
|
self.assertEqual(report_tags.add(m1, m2), Money(150, 'USD'))
|
||||||
self.assertEqual(report_tags.subtract(m1, m2), Money(50, 'USD'))
|
self.assertEqual(report_tags.subtract(m1, m2), Money(50, 'USD'))
|
||||||
self.assertEqual(report_tags.multiply(m2, 3), Money(150, 'USD'))
|
self.assertEqual(report_tags.multiply(m2, 3), Money(150, 'USD'))
|
||||||
|
self.assertEqual(report_tags.multiply(-3, m2), Money(-150, 'USD'))
|
||||||
self.assertEqual(report_tags.divide(m1, '4'), Money(25, 'USD'))
|
self.assertEqual(report_tags.divide(m1, '4'), Money(25, 'USD'))
|
||||||
|
|
||||||
|
result = report_tags.divide(Money(1000, 'GBP'), 4)
|
||||||
|
self.assertIsInstance(result, Money)
|
||||||
|
|
||||||
def test_maths_tags_invalid(self):
|
def test_maths_tags_invalid(self):
|
||||||
"""Tests for mathematical operator tags with invalid inputs."""
|
"""Tests for mathematical operator tags with invalid inputs."""
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(ValidationError):
|
||||||
report_tags.add('abc', 10)
|
report_tags.add('abc', 10)
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(ValidationError):
|
||||||
report_tags.subtract(50, 'xyz')
|
report_tags.subtract(50, 'xyz')
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(ValidationError):
|
||||||
report_tags.multiply('foo', 'bar')
|
report_tags.multiply('foo', 'bar')
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(ValidationError):
|
||||||
report_tags.divide('100', 'baz')
|
report_tags.divide('100', 'baz')
|
||||||
|
|
||||||
def test_number_tags(self):
|
def test_number_tags(self):
|
||||||
@@ -410,8 +421,19 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
'$1,234.0',
|
'$1,234.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test with non-currency values
|
||||||
|
self.assertEqual(
|
||||||
|
report_tags.render_currency(1234.45, currency='USD', decimal_places=2),
|
||||||
|
'$1,234.45',
|
||||||
|
)
|
||||||
|
|
||||||
# Test with an invalid amount
|
# Test with an invalid amount
|
||||||
self.assertEqual(report_tags.render_currency('abc'), '-')
|
with self.assertRaises(ValidationError):
|
||||||
|
report_tags.render_currency('abc', currency='-')
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
report_tags.render_currency(m, multiplier='quork')
|
||||||
|
|
||||||
self.assertEqual(report_tags.render_currency(m, decimal_places='a'), exp_m)
|
self.assertEqual(report_tags.render_currency(m, decimal_places='a'), exp_m)
|
||||||
self.assertEqual(report_tags.render_currency(m, min_decimal_places='a'), exp_m)
|
self.assertEqual(report_tags.render_currency(m, min_decimal_places='a'), exp_m)
|
||||||
self.assertEqual(report_tags.render_currency(m, max_decimal_places='a'), exp_m)
|
self.assertEqual(report_tags.render_currency(m, max_decimal_places='a'), exp_m)
|
||||||
|
|||||||
@@ -587,9 +587,11 @@ export function ApiForm({
|
|||||||
<Alert radius='sm' color='red' title={t`Form Error`}>
|
<Alert radius='sm' color='red' title={t`Form Error`}>
|
||||||
{nonFieldErrors.length > 0 ? (
|
{nonFieldErrors.length > 0 ? (
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
{nonFieldErrors.map((message) => (
|
{nonFieldErrors
|
||||||
<Text key={message}>{message}</Text>
|
.filter((message) => !!message && message !== 'None')
|
||||||
))}
|
.map((message) => (
|
||||||
|
<Text key={message}>{message}</Text>
|
||||||
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Text>{t`Errors exist for one or more form fields`}</Text>
|
<Text>{t`Errors exist for one or more form fields`}</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user