From e09b74b171b7ba271a6466f908e508edffbaacb9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 19 Jun 2026 23:31:09 +0000 Subject: [PATCH] Cleanup duplicate code --- src/backend/InvenTree/InvenTree/format.py | 67 -------------- .../InvenTree/InvenTree/helpers_model.py | 89 ------------------- src/backend/InvenTree/InvenTree/tests.py | 29 ------ .../InvenTree/report/templatetags/report.py | 21 ++++- 4 files changed, 20 insertions(+), 186 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/format.py b/src/backend/InvenTree/InvenTree/format.py index d5a0ca5f32..848c7e07ae 100644 --- a/src/backend/InvenTree/InvenTree/format.py +++ b/src/backend/InvenTree/InvenTree/format.py @@ -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 diff --git a/src/backend/InvenTree/InvenTree/helpers_model.py b/src/backend/InvenTree/InvenTree/helpers_model.py index 480337f397..925046aef1 100644 --- a/src/backend/InvenTree/InvenTree/helpers_model.py +++ b/src/backend/InvenTree/InvenTree/helpers_model.py @@ -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. diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index 38062926d5..ea2f6d4171 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -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.""" diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 867222bbd8..bf49e55c0b 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -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.