diff --git a/src/pretix/base/templatetags/money.py b/src/pretix/base/templatetags/money.py index bb3f674e73..2dbe25a6e2 100644 --- a/src/pretix/base/templatetags/money.py +++ b/src/pretix/base/templatetags/money.py @@ -48,19 +48,12 @@ def money_filter(value: Decimal, arg='', hide_currency=False): raise ValueError("No currency passed.") arg = arg.upper() - places = settings.CURRENCY_PLACES.get(arg, 2) - rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP) - if places < 2 and rounded != value: - # We display decimal places even if we shouldn't for this currency if rounding - # would make the numbers incorrect. If this branch executes, it's likely a bug in - # pretix, but we won't show wrong numbers! - if hide_currency: - return floatformat(value, "2g") - else: - return '{} {}'.format(arg, floatformat(value, "2g")) + currency_places = settings.CURRENCY_PLACES.get(arg, 2) + required_places = -value.normalize().as_tuple().exponent + render_places = max(currency_places, required_places) if hide_currency: - return floatformat(value, f"{places}g") + return floatformat(value, f"{render_places}g") try: locale = Locale(get_babel_locale()) @@ -68,9 +61,24 @@ def money_filter(value: Decimal, arg='', hide_currency=False): locale = "en" try: - return format_currency(value, arg, locale=locale) + return format_currency( + value, + arg, + locale=locale, + # We only allow Babel to restrict the digits to the digits by the currency if this does not remove any + # precision in case we have sub-currency precision (which we shouldn't have in most places, but it's still + # better than showing wrong data). Note: Weird precision effects can occur after in-database arithmetic + # on SQLite, since SQLite does not have fixed-decimal computation. + currency_digits=currency_places >= required_places, + decimal_quantization=currency_places >= required_places, + ) except: - return '{} {}'.format(arg, floatformat(value, f"{places}g")) + return '{} {}'.format(arg, floatformat(value, f"{render_places}g")) + + +@register.filter("money_without_currency") +def money_filter_without_currency(value: Decimal, arg=''): + return money_filter(value, arg, hide_currency=True) @register.filter("money_numberfield") @@ -91,11 +99,18 @@ def tax_rate_format(number): """ Display a Decimal to its significant decimal places, used for tax rates. """ - assert isinstance(number, Decimal) + if isinstance(number, (float, int, str)): + number = Decimal(number) + if number is None: + number = Decimal('0.00') + if not isinstance(number, Decimal): + if number == '': + return number + raise TypeError("Invalid data type passed to tax rate format filter: %r" % type(number)) return mark_safe( formats.number_format( - number.normalize(), - -number.as_tuple().exponent, + number, + -number.normalize().as_tuple().exponent, use_l10n=True, force_grouping=False, ) diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index ca6d5f11f8..6ca7d8d084 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -108,7 +108,9 @@ from pretix.base.services.export import ( init_organizer_exporters, multiexport, scheduled_organizer_export, ) from pretix.base.services.mail import mail, prefix_subject -from pretix.base.services.placeholders import prepare_sample_context_for_preview +from pretix.base.services.placeholders import ( + prepare_sample_context_for_preview, +) from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.views.tasks import AsyncAction from pretix.control.forms.exports import ScheduledOrganizerExportForm diff --git a/src/tests/base/test_templatetag.py b/src/tests/base/test_templatetag.py index d2041e5d38..a4b5cd3ef0 100644 --- a/src/tests/base/test_templatetag.py +++ b/src/tests/base/test_templatetag.py @@ -26,7 +26,7 @@ from django.template import Context, Template from django.test import RequestFactory from django.utils import translation -from pretix.base.templatetags.money import money_filter +from pretix.base.templatetags.money import money_filter, tax_rate_format TEMPLATE_REPLACE_PAGE = Template( "{% load urlreplace %}{% url_replace request 'page' 3 %}" @@ -70,11 +70,13 @@ def test_urlreplace_replace_parameter(): # unknown currency ("de", Decimal("1234.56"), "FOO", "1.234,56" + NBSP + "FOO"), - ("de", Decimal("1234.567"), "FOO", "1.234,57" + NBSP + "FOO"), + ("de", Decimal("1234.567"), "FOO", "1.234,567" + NBSP + "FOO"), - # rounding errors - ("de", Decimal("1.234"), "EUR", "1,23" + NBSP + "€"), - ("de", Decimal("1023.1"), "JPY", "JPY 1.023,10"), + # deal with precision that is higher than the currency + ("de", Decimal("1.234"), "EUR", "1,234" + NBSP + "€"), + ("de", Decimal("1.2340"), "EUR", "1,234" + NBSP + "€"), + ("de", Decimal("1.2300"), "EUR", "1,23" + NBSP + "€"), + ("de", Decimal("1023.1"), "JPY", "1.023,10" + NBSP + "¥"), ] ) def test_money_filter(locale, amount, currency, expected): @@ -98,9 +100,26 @@ def test_money_filter(locale, amount, currency, expected): [ ("de", Decimal("1000.00"), "EUR", "1.000,00"), ("en", Decimal("1000.00"), "EUR", "1,000.00"), - ("de", Decimal("1023.1"), "JPY", "1.023,10"), + ("de", Decimal("1023.1"), "JPY", "1.023,1"), ] ) def test_money_filter_hidecurrency(locale, amount, currency, expected): translation.activate(locale) assert money_filter(amount, currency, hide_currency=True) == expected + + +@pytest.mark.parametrize( + "locale,rate,expected", + [ + ("de", Decimal("2.00"), "2"), + ("de", Decimal("2.50"), "2,5"), + ("de", Decimal("2.2340"), "2,234"), + ("en", Decimal("2.00"), "2"), + ("en", Decimal("2.50"), "2.5"), + ("en", Decimal("4.3e7"), "43000000"), + ("en", Decimal("2.2340"), "2.234"), + ] +) +def test_tax_rate_format(locale, rate, expected): + translation.activate(locale) + assert tax_rate_format(rate) == expected