Refactoring:
- Change REPORT_CURRENCY_LOCALE to REPORT_LOCALE
This commit is contained in:
parent
c56a9cc335
commit
72c58a4a7a
|
|
@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- [#12208](https://github.com/inventree/InvenTree/pull/12208) adds custom locale support for currency rendering within reports.
|
||||
- [#12208](https://github.com/inventree/InvenTree/pull/12208) adds custom locale support for rendering currencies, dates and numbers within reports. This allows users to specify a custom locale for report rendering, which can be used to control the formatting of dates, numbers and currency values in the generated reports. The custom locale can be set via the `REPORT_LOCALE` system setting, and if not set, the system will fall back to using the default system locale for report rendering.
|
||||
- [#12204](https://github.com/inventree/InvenTree/pull/12204) adds new filtering options to PartCategoryTree and StockLocationTree API endpoints, allowing tree data to be fetched dynamically
|
||||
- [#12165](https://github.com/inventree/InvenTree/pull/12165) adds support for parameters against the PartCategory model
|
||||
- [#12103](https://github.com/inventree/InvenTree/pull/12103) adds column-based filtering to table views in the user interface. This extends the existing table filtering functionality by allowing users to apply filters directly to individual columns.
|
||||
|
|
|
|||
|
|
@ -393,10 +393,10 @@ The locale controls how the currency symbol is rendered. Different locales produ
|
|||
The locale used by `render_currency` is resolved in the following priority order:
|
||||
|
||||
1. **Explicit `locale=` argument** in the template tag — highest priority, always wins
|
||||
2. **`REPORT_CURRENCY_LOCALE` global setting** — applied to all reports when set, with no per-tag argument needed
|
||||
2. **`REPORT_LOCALE` global setting** — applied to all reports when set, with no per-tag argument needed
|
||||
3. **Server `LANGUAGE_CODE`** — the fallback when neither of the above is configured
|
||||
|
||||
To apply a consistent locale across all reports without modifying every template, set the `REPORT_CURRENCY_LOCALE` global setting (via *Settings → Reporting → Report Currency Locale*). Individual templates can still override it per-tag using the `locale=` argument.
|
||||
To apply a consistent locale across all reports without modifying every template, set the `REPORT_LOCALE` global setting (via *Settings → Reporting → Report Currency Locale*). Individual templates can still override it per-tag using the `locale=` argument.
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -407,7 +407,7 @@ To apply a consistent locale across all reports without modifying every template
|
|||
<em>Line Item Unit Pricing:</em>
|
||||
<ul>
|
||||
{% for line in order.lines %}
|
||||
<!-- Locale resolved from REPORT_CURRENCY_LOCALE setting, or server LANGUAGE_CODE -->
|
||||
<!-- Locale resolved from REPORT_LOCALE setting, or server LANGUAGE_CODE -->
|
||||
<li>{% render_currency line.price currency=order.supplier.currency %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ Configuration of report generation:
|
|||
| Name | Description | Default | Units |
|
||||
| ---- | ----------- | ------- | ----- |
|
||||
{{ globalsetting("REPORT_ENABLE") }}
|
||||
{{ globalsetting("REPORT_CURRENCY_LOCALE") }}
|
||||
{{ globalsetting("REPORT_LOCALE") }}
|
||||
{{ globalsetting("REPORT_DEFAULT_PAGE_SIZE") }}
|
||||
{{ globalsetting("REPORT_DEBUG_MODE") }}
|
||||
{{ globalsetting("REPORT_FETCH_URLS") }}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,22 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
|||
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,
|
||||
|
|
@ -206,22 +222,18 @@ def format_money(
|
|||
Raises:
|
||||
ValueError: format string is incorrectly specified
|
||||
"""
|
||||
language = locale or settings.LANGUAGE_CODE
|
||||
try:
|
||||
locale = Locale.parse(translation.to_locale(language))
|
||||
except (UnknownLocaleError, ValueError) as e:
|
||||
raise ValidationError(f"format_money: Invalid locale '{language}' - {e}")
|
||||
resolved_locale = get_locale(locale)
|
||||
|
||||
if fmt:
|
||||
pattern = parse_pattern(fmt)
|
||||
else:
|
||||
pattern = locale.currency_formats['standard']
|
||||
pattern = resolved_locale.currency_formats['standard']
|
||||
if decimal_places is not None:
|
||||
pattern.frac_prec = (decimal_places, decimal_places)
|
||||
|
||||
result = pattern.apply(
|
||||
money.amount,
|
||||
locale,
|
||||
resolved_locale,
|
||||
currency=money.currency.code if include_symbol else '',
|
||||
currency_digits=decimal_places is None,
|
||||
decimal_quantization=decimal_places is not None,
|
||||
|
|
|
|||
|
|
@ -671,10 +671,10 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'REPORT_CURRENCY_LOCALE': {
|
||||
'name': _('Report Currency Locale'),
|
||||
'REPORT_LOCALE': {
|
||||
'name': _('Report Locale'),
|
||||
'description': _(
|
||||
'Locale to use when rendering currency values in reports (e.g. en-US, fr-FR). Leave blank to use the system locale setting.'
|
||||
'Locale to use when rendering dates, numbers, and currency values in reports (e.g. en-US, de-DE). Leave blank to use the system locale setting.'
|
||||
),
|
||||
'default': '',
|
||||
'validator': common.validators.validate_locale,
|
||||
|
|
|
|||
|
|
@ -621,10 +621,8 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
|||
self.assertEqual(codes, 'AUD,USD,GBP')
|
||||
|
||||
def test_report_currency_locale(self):
|
||||
"""Test validation of the REPORT_CURRENCY_LOCALE setting."""
|
||||
url = reverse(
|
||||
'api-global-setting-detail', kwargs={'key': 'REPORT_CURRENCY_LOCALE'}
|
||||
)
|
||||
"""Test validation of the REPORT_LOCALE setting (locale string for report formatting)."""
|
||||
url = reverse('api-global-setting-detail', kwargs={'key': 'REPORT_LOCALE'})
|
||||
|
||||
# Valid locale values are accepted
|
||||
for valid in ['en-us', 'en-gb', 'en-au', 'de-de', 'fr-fr']:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ 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
|
||||
|
|
@ -19,6 +20,9 @@ from django.db.models.query import QuerySet
|
|||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
|
|
@ -32,6 +36,7 @@ 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()
|
||||
|
|
@ -771,11 +776,21 @@ def modulo(x: Any, y: Any, cast: Optional[type] = None) -> Any:
|
|||
return cast_to_type(result, cast)
|
||||
|
||||
|
||||
def _get_report_locale(override: Optional[str] = None) -> Optional[str]:
|
||||
"""Return the locale to use for report formatting.
|
||||
|
||||
Priority: explicit override > REPORT_LOCALE global setting > None (caller falls back to LANGUAGE_CODE).
|
||||
"""
|
||||
if override:
|
||||
return override
|
||||
return get_global_setting('REPORT_LOCALE', cache=True) or settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def render_currency(money, **kwargs):
|
||||
"""Render a currency / Money object."""
|
||||
if 'locale' not in kwargs:
|
||||
if locale := get_global_setting('REPORT_CURRENCY_LOCALE', cache=True):
|
||||
if locale := _get_report_locale():
|
||||
kwargs['locale'] = locale
|
||||
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||
|
||||
|
|
@ -879,6 +894,7 @@ def format_number(
|
|||
integer: bool = False,
|
||||
leading: int = 0,
|
||||
separator: Optional[str] = None,
|
||||
locale: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Render a number with optional formatting options.
|
||||
|
||||
|
|
@ -888,7 +904,8 @@ def format_number(
|
|||
multiplier: Optional multiplier to apply to the number before formatting
|
||||
integer: Boolean, whether to render the number as an integer
|
||||
leading: Number of leading zeros (default = 0)
|
||||
separator: Character to use as a thousands separator (default = None)
|
||||
separator: Character to use as a thousands separator (default = None, ignored when locale is active)
|
||||
locale: Optional locale override (e.g. 'en-us', 'de-de'). When set, babel controls decimal and thousands separators.
|
||||
"""
|
||||
check_nulls('format_number', number)
|
||||
|
||||
|
|
@ -902,33 +919,35 @@ def format_number(
|
|||
number *= Decimal(str(multiplier).strip())
|
||||
|
||||
if integer:
|
||||
# Convert to integer
|
||||
number = Decimal(int(number))
|
||||
|
||||
# Normalize the number (remove trailing zeroes)
|
||||
number = number.normalize()
|
||||
|
||||
if decimal_places is not None:
|
||||
try:
|
||||
decimal_places = int(decimal_places)
|
||||
number = round(number, decimal_places)
|
||||
except ValueError:
|
||||
pass
|
||||
decimal_places = None
|
||||
|
||||
# Re-encode, and normalize again
|
||||
# Ensure that the output never uses scientific notation
|
||||
value = Decimal(number)
|
||||
value = (
|
||||
value.quantize(Decimal(1))
|
||||
if value == value.to_integral()
|
||||
else value.normalize()
|
||||
)
|
||||
resolved_locale = _get_report_locale(locale)
|
||||
|
||||
if separator:
|
||||
value = f'{value:,}'
|
||||
value = value.replace(',', separator)
|
||||
if resolved_locale:
|
||||
babel_locale = get_locale(resolved_locale)
|
||||
fmt = '#,##0.' + '0' * decimal_places if decimal_places is not None else None
|
||||
value = babel_format_decimal(number, format=fmt, locale=babel_locale)
|
||||
else:
|
||||
value = f'{value}'
|
||||
# Normalize the number (remove trailing zeroes), avoid scientific notation
|
||||
value = Decimal(number)
|
||||
value = (
|
||||
value.quantize(Decimal(1))
|
||||
if value == value.to_integral()
|
||||
else value.normalize()
|
||||
)
|
||||
|
||||
if separator:
|
||||
value = f'{value:,}'
|
||||
value = value.replace(',', separator)
|
||||
else:
|
||||
value = f'{value}'
|
||||
|
||||
if leading is not None:
|
||||
try:
|
||||
|
|
@ -942,14 +961,18 @@ def format_number(
|
|||
|
||||
@register.simple_tag
|
||||
def format_datetime(
|
||||
dt: datetime, timezone: Optional[str] = None, fmt: Optional[str] = None
|
||||
dt: datetime,
|
||||
timezone: Optional[str] = None,
|
||||
fmt: Optional[str] = None,
|
||||
locale: Optional[str] = None,
|
||||
):
|
||||
"""Format a datetime object for display.
|
||||
|
||||
Arguments:
|
||||
dt: The datetime object to format
|
||||
timezone: The timezone to use for the date (defaults to the server timezone)
|
||||
fmt: The format string to use (defaults to ISO formatting)
|
||||
fmt: The strftime format string to use. When provided, takes priority over locale.
|
||||
locale: Optional locale override (e.g. 'en-us', 'de-de'). Used for locale-aware formatting when no fmt is given.
|
||||
"""
|
||||
check_nulls('format_datetime', dt)
|
||||
|
||||
|
|
@ -957,18 +980,30 @@ def format_datetime(
|
|||
|
||||
if fmt:
|
||||
return dt.strftime(fmt)
|
||||
else:
|
||||
return dt.isoformat()
|
||||
|
||||
resolved_locale = _get_report_locale(locale)
|
||||
if resolved_locale:
|
||||
return babel_format_datetime(
|
||||
dt, format='medium', locale=get_locale(resolved_locale)
|
||||
)
|
||||
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def format_date(dt: date, timezone: Optional[str] = None, fmt: Optional[str] = None):
|
||||
def format_date(
|
||||
dt: date,
|
||||
timezone: Optional[str] = None,
|
||||
fmt: Optional[str] = None,
|
||||
locale: Optional[str] = None,
|
||||
):
|
||||
"""Format a date object for display.
|
||||
|
||||
Arguments:
|
||||
dt: The date to format
|
||||
timezone: The timezone to use for the date (defaults to the server timezone)
|
||||
fmt: The format string to use (defaults to ISO formatting)
|
||||
fmt: The strftime format string to use. When provided, takes priority over locale.
|
||||
locale: Optional locale override (e.g. 'en-us', 'de-de'). Used for locale-aware formatting when no fmt is given.
|
||||
"""
|
||||
check_nulls('format_date', dt)
|
||||
|
||||
|
|
@ -979,8 +1014,14 @@ def format_date(dt: date, timezone: Optional[str] = None, fmt: Optional[str] = N
|
|||
|
||||
if fmt:
|
||||
return dt.strftime(fmt)
|
||||
else:
|
||||
return dt.isoformat()
|
||||
|
||||
resolved_locale = _get_report_locale(locale)
|
||||
if resolved_locale:
|
||||
return babel_format_date(
|
||||
dt, format='medium', locale=get_locale(resolved_locale)
|
||||
)
|
||||
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export default function SystemSettings() {
|
|||
<GlobalSettingList
|
||||
keys={[
|
||||
'REPORT_ENABLE',
|
||||
'REPORT_CURRENCY_LOCALE',
|
||||
'REPORT_LOCALE',
|
||||
'REPORT_DEFAULT_PAGE_SIZE',
|
||||
'REPORT_DEBUG_MODE',
|
||||
'REPORT_FETCH_URLS',
|
||||
|
|
|
|||
Loading…
Reference in New Issue