Cleanup duplicate code
This commit is contained in:
parent
689d9fe30c
commit
e09b74b171
|
|
@ -2,18 +2,9 @@
|
|||
|
||||
import re
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import translation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from babel import Locale
|
||||
from babel.core import UnknownLocaleError
|
||||
from babel.numbers import parse_pattern
|
||||
from djmoney.money import Money
|
||||
|
||||
|
||||
def parse_format_string(fmt_string: str) -> dict:
|
||||
"""Extract formatting information from the provided format string.
|
||||
|
|
@ -182,61 +173,3 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
|||
# And return the value we are interested in
|
||||
# Note: This will raise an IndexError if the named group was not matched
|
||||
return result.group(name)
|
||||
|
||||
|
||||
def get_locale(locale: Optional[str] = None) -> Locale:
|
||||
"""Resolve and return a babel Locale.
|
||||
|
||||
Args:
|
||||
locale: Optional locale string (e.g. 'en-us'). Falls back to LANGUAGE_CODE.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the locale string is invalid.
|
||||
"""
|
||||
language = locale or settings.LANGUAGE_CODE
|
||||
try:
|
||||
return Locale.parse(translation.to_locale(language))
|
||||
except (UnknownLocaleError, ValueError) as e:
|
||||
raise ValidationError(f"Invalid locale '{language}' - {e}")
|
||||
|
||||
|
||||
def format_money(
|
||||
money: Money,
|
||||
decimal_places: Optional[int] = None,
|
||||
fmt: Optional[str] = None,
|
||||
include_symbol: bool = True,
|
||||
locale: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Format money object according to the currently set local.
|
||||
|
||||
Args:
|
||||
money (Money): The money object to format
|
||||
decimal_places (int): Number of decimal places to use
|
||||
fmt (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
|
||||
include_symbol (bool): Whether to include the currency symbol in the formatted output
|
||||
locale (str): Optional locale override (e.g. 'en-us', 'en-gb'). Defaults to the server LANGUAGE_CODE setting.
|
||||
|
||||
Returns:
|
||||
str: The formatted string
|
||||
|
||||
Raises:
|
||||
ValueError: format string is incorrectly specified
|
||||
"""
|
||||
resolved_locale = get_locale(locale)
|
||||
|
||||
if fmt:
|
||||
pattern = parse_pattern(fmt)
|
||||
else:
|
||||
pattern = resolved_locale.currency_formats['standard']
|
||||
if decimal_places is not None:
|
||||
pattern.frac_prec = (decimal_places, decimal_places)
|
||||
|
||||
result = pattern.apply(
|
||||
money.amount,
|
||||
resolved_locale,
|
||||
currency=money.currency.code if include_symbol else '',
|
||||
currency_digits=decimal_places is None,
|
||||
decimal_quantization=decimal_places is not None,
|
||||
)
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@
|
|||
import io
|
||||
import ipaddress
|
||||
import socket
|
||||
from decimal import Decimal
|
||||
from typing import Optional, cast
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -16,8 +14,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
import requests
|
||||
import requests.exceptions
|
||||
import structlog
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
from common.notifications import (
|
||||
|
|
@ -31,7 +27,6 @@ from InvenTree.cache import (
|
|||
get_session_cache,
|
||||
set_session_cache,
|
||||
)
|
||||
from InvenTree.format import format_money
|
||||
from InvenTree.ready import ignore_ready_warning
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
|
@ -252,90 +247,6 @@ def download_image_from_url(
|
|||
return img
|
||||
|
||||
|
||||
def render_currency(
|
||||
money: Money,
|
||||
decimal_places: Optional[int] = None,
|
||||
currency: Optional[str] = None,
|
||||
multiplier: Optional[Decimal] = None,
|
||||
min_decimal_places: Optional[int] = None,
|
||||
max_decimal_places: Optional[int] = None,
|
||||
include_symbol: bool = True,
|
||||
locale: Optional[str] = None,
|
||||
):
|
||||
"""Render a currency / Money object to a formatted string (e.g. for reports).
|
||||
|
||||
Arguments:
|
||||
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.
|
||||
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.
|
||||
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
|
||||
locale: Optional locale override (e.g. 'en-us', 'en-gb'). Controls symbol vs. code rendering. Defaults to server LANGUAGE_CODE.
|
||||
"""
|
||||
if money in [None, '']:
|
||||
return '-'
|
||||
|
||||
if type(money) is not Money:
|
||||
# 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:
|
||||
# Attempt to convert to the provided currency
|
||||
# If cannot be done, leave the original
|
||||
try:
|
||||
money = convert_money(money, currency)
|
||||
except Exception:
|
||||
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)):
|
||||
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||
|
||||
if max_decimal_places is None or not isinstance(max_decimal_places, (int, float)):
|
||||
max_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
value = Decimal(str(money.amount)).normalize()
|
||||
value = str(value)
|
||||
|
||||
if decimal_places is not None and isinstance(decimal_places, (int, float)):
|
||||
# Decimal place count is provided, use it
|
||||
pass
|
||||
elif '.' in value:
|
||||
# If the value has a decimal point, use the number of decimal places in the value
|
||||
decimal_places = len(value.split('.')[-1])
|
||||
else:
|
||||
# No decimal point, use 2 as a default
|
||||
decimal_places = 2
|
||||
|
||||
# Clip the decimal places to the specified range
|
||||
decimal_places = max(decimal_places, min_decimal_places)
|
||||
decimal_places = min(decimal_places, max_decimal_places)
|
||||
|
||||
return format_money(
|
||||
money,
|
||||
decimal_places=decimal_places,
|
||||
include_symbol=include_symbol,
|
||||
locale=locale,
|
||||
)
|
||||
|
||||
|
||||
@ignore_ready_warning
|
||||
def getModelsWithMixin(mixin_class) -> list:
|
||||
"""Return a list of database models that inherit from the given mixin class.
|
||||
|
|
|
|||
|
|
@ -608,35 +608,6 @@ class FormatTest(TestCase):
|
|||
with self.assertRaises(ValueError):
|
||||
InvenTree.format.extract_named_group('test', 'PO-ABC-xyz', 'PO-###-{test}')
|
||||
|
||||
def test_currency_formatting(self):
|
||||
"""Test that currency formatting works correctly for multiple currencies."""
|
||||
test_data = (
|
||||
(Money(3651.285718, 'USD'), 4, True, '$3,651.2857'),
|
||||
(Money(487587.849178, 'CAD'), 5, True, 'CA$487,587.84918'),
|
||||
(Money(0.348102, 'EUR'), 1, False, '0.3'),
|
||||
(Money(0.916530, 'GBP'), 1, True, '£0.9'),
|
||||
(Money(61.031024, 'JPY'), 3, False, '61.031'),
|
||||
(Money(49609.694602, 'JPY'), 1, True, '¥49,609.7'),
|
||||
(Money(155565.264777, 'AUD'), 2, False, '155,565.26'),
|
||||
(Money(0.820437, 'CNY'), 4, True, 'CN¥0.8204'),
|
||||
(Money(7587.849178, 'EUR'), 0, True, '€7,588'),
|
||||
(Money(0.348102, 'GBP'), 3, False, '0.348'),
|
||||
(Money(0.652923, 'CHF'), 0, True, 'CHF1'),
|
||||
(Money(0.820437, 'CNY'), 1, True, 'CN¥0.8'),
|
||||
(Money(98789.5295680, 'CHF'), 0, False, '98,790'),
|
||||
(Money(0.585787, 'USD'), 1, True, '$0.6'),
|
||||
(Money(0.690541, 'CAD'), 3, True, 'CA$0.691'),
|
||||
(Money(427.814104, 'AUD'), 5, True, 'A$427.81410'),
|
||||
)
|
||||
|
||||
with self.settings(LANGUAGE_CODE='en-us'):
|
||||
for value, decimal_places, include_symbol, expected_result in test_data:
|
||||
result = InvenTree.format.format_money(
|
||||
value, decimal_places=decimal_places, include_symbol=include_symbol
|
||||
)
|
||||
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
|
||||
class TestHelpers(TestCase):
|
||||
"""Tests for InvenTree helper functions."""
|
||||
|
|
|
|||
|
|
@ -11,14 +11,18 @@ from typing import Any, Optional
|
|||
|
||||
from django import template
|
||||
from django.apps.registry import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.exceptions import SuspiciousFileOperation, ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import translation
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from babel import Locale
|
||||
from babel.core import UnknownLocaleError
|
||||
from babel.dates import format_date as babel_format_date
|
||||
from babel.dates import format_datetime as babel_format_datetime
|
||||
from babel.numbers import format_decimal as babel_format_decimal
|
||||
|
|
@ -36,7 +40,6 @@ import InvenTree.helpers_model
|
|||
import report.helpers
|
||||
from common.settings import get_global_setting
|
||||
from company.models import Company
|
||||
from InvenTree.format import get_locale
|
||||
from part.models import Part
|
||||
|
||||
register = template.Library()
|
||||
|
|
@ -45,6 +48,22 @@ register = template.Library()
|
|||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def get_locale(locale: Optional[str] = None) -> Locale:
|
||||
"""Resolve and return a babel Locale.
|
||||
|
||||
Args:
|
||||
locale: Optional locale string (e.g. 'en-us'). Falls back to LANGUAGE_CODE.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the locale string is invalid.
|
||||
"""
|
||||
language = locale or settings.LANGUAGE_CODE
|
||||
try:
|
||||
return Locale.parse(translation.to_locale(language))
|
||||
except (UnknownLocaleError, ValueError) as e:
|
||||
raise ValidationError(f"Invalid locale '{language}' - {e}")
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def order_queryset(queryset: QuerySet, *args) -> QuerySet:
|
||||
"""Order a database queryset based on the provided arguments.
|
||||
|
|
|
|||
Loading…
Reference in New Issue