diff --git a/src/pretix/api/serializers/__init__.py b/src/pretix/api/serializers/__init__.py
index ea435914ac..16e8bb7e96 100644
--- a/src/pretix/api/serializers/__init__.py
+++ b/src/pretix/api/serializers/__init__.py
@@ -20,6 +20,7 @@
# .
#
import json
+import re
from django.db.models import prefetch_related_objects
from rest_framework import serializers
@@ -135,3 +136,30 @@ class SalesChannelMigrationMixin:
else:
value["sales_channels"] = value["limit_sales_channels"]
return value
+
+
+class CompatDecimalField(serializers.DecimalField):
+ """
+ Historically, pretix recorded tax rates as decimals with two places. Today, pretix supports tax rates with up to
+ four places. Since our API outputs decimals with the stored precision, this would have changed the API output from
+ "19.00" to "19.0000" without warning. While this is semantically the same thing, we need to assume some pretix API
+ users might run into trouble, either because they treat the value as a string and then map something
+ (e.g. ``if tax_rate == "19.00"``) or process it with a language where this is a significant difference. For example,
+ while in Python ``Decimal("19.00") == Decimal("19.0000")`` is true, in Java
+ ``(new BigDecimal("19.00")).equals(new BigDecimal("19.0000"))`` is false and only
+ ``(new BigDecimal("19.00")).compareTo(new BigDecimal("19.0000")) == 0`` is true.
+
+ Therefore, we stay backwards compatible by outputting two decimal places *as long as the trailing digits are zero-valued.
+ """
+
+ regex = re.compile(r"^([0-9]+\.[0-9]{2})0+$")
+
+ def to_representation(self, value):
+ if self.localize:
+ raise ValueError("localization not supported")
+ value = super().to_representation(value)
+ if value and "." not in value:
+ return f"{value}.00"
+ if m := self.regex.match(value):
+ return m.group(1)
+ return value
diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py
index 4276b756b9..73e0b9c518 100644
--- a/src/pretix/api/serializers/event.py
+++ b/src/pretix/api/serializers/event.py
@@ -48,7 +48,7 @@ from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import (
- CompatibleJSONField, SalesChannelMigrationMixin,
+ CompatDecimalField, CompatibleJSONField, SalesChannelMigrationMixin,
)
from pretix.api.serializers.fields import PluginsField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -681,6 +681,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
required=False,
allow_null=True,
)
+ rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = TaxRule
diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py
index 35750c02df..dacf056b6a 100644
--- a/src/pretix/api/serializers/item.py
+++ b/src/pretix/api/serializers/item.py
@@ -42,7 +42,9 @@ from django.utils.functional import cached_property, lazy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
-from pretix.api.serializers import SalesChannelMigrationMixin
+from pretix.api.serializers import (
+ CompatDecimalField, SalesChannelMigrationMixin,
+)
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -276,10 +278,10 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
return value
-class ItemTaxRateField(serializers.Field):
+class ItemTaxRateField(CompatDecimalField):
def to_representation(self, i):
if i.tax_rule:
- return str(Decimal(i.tax_rule.rate))
+ return super().to_representation(Decimal(i.tax_rule.rate))
else:
return str(Decimal('0.00'))
@@ -289,7 +291,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
- tax_rate = ItemTaxRateField(source='*', read_only=True)
+ tax_rate = ItemTaxRateField(source='*', read_only=True, max_digits=7, decimal_places=4)
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
'image/png', 'image/jpeg', 'image/gif'
diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index 83533a73c6..704b24f496 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -41,7 +41,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
-from pretix.api.serializers import CompatibleJSONField
+from pretix.api.serializers import CompatDecimalField, CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.forms import form_field_to_serializer_field
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -591,6 +591,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
attendee_name = serializers.CharField(required=False)
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
+ tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
list_serializer_class = OrderPositionListSerializer
@@ -747,6 +748,8 @@ class OrderPaymentDateField(serializers.DateField):
class OrderFeeSerializer(I18nAwareModelSerializer):
+ tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
+
class Meta:
model = OrderFee
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
@@ -1911,6 +1914,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
position = LinePositionField(read_only=True)
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
+ tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = InvoiceLine
@@ -1994,6 +1998,7 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
class TransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
+ tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = Transaction
diff --git a/src/pretix/base/migrations/0304_tax_rate_decimals.py b/src/pretix/base/migrations/0304_tax_rate_decimals.py
new file mode 100644
index 0000000000..d3ca664441
--- /dev/null
+++ b/src/pretix/base/migrations/0304_tax_rate_decimals.py
@@ -0,0 +1,48 @@
+# Generated by Django 5.2.12 on 2026-04-15 20:10
+
+from decimal import Decimal
+
+from django.db import migrations, models
+
+import pretix.helpers.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0303_alter_order_organizer_alter_orderposition_organizer'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='cartposition',
+ name='tax_rate',
+ field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
+ ),
+ migrations.AlterField(
+ model_name='invoiceline',
+ name='tax_rate',
+ field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
+ ),
+ migrations.AlterField(
+ model_name='orderfee',
+ name='tax_rate',
+ field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
+ ),
+ migrations.AlterField(
+ model_name='orderposition',
+ name='tax_rate',
+ field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
+ ),
+ migrations.AlterField(
+ model_name='transaction',
+ name='tax_rate',
+ field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
+ ),
+ migrations.AlterField(
+ model_name='taxrule',
+ name='rate',
+ field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
+ ),
+
+ ]
diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py
index e4e9eaa6c2..d0787b7321 100644
--- a/src/pretix/base/models/invoices.py
+++ b/src/pretix/base/models/invoices.py
@@ -49,6 +49,7 @@ from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.helpers.countries import FastCountryField
+from pretix.helpers.models import NormalizedDecimalField
def invoice_filename(instance, filename: str) -> str:
@@ -450,7 +451,7 @@ class InvoiceLine(models.Model):
description = models.TextField()
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
- tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
+ tax_rate = NormalizedDecimalField(max_digits=7, decimal_places=4, default=Decimal('0'))
tax_name = models.CharField(max_length=190)
tax_code = models.CharField(max_length=190, null=True, blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index a0b1d63bf9..626e85868c 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -87,6 +87,7 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
+from ...helpers.models import NormalizedDecimalField
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -2354,8 +2355,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
)
description = models.CharField(max_length=190, blank=True)
internal_type = models.CharField(max_length=255, blank=True)
- tax_rate = models.DecimalField(
- max_digits=7, decimal_places=2,
+ tax_rate = NormalizedDecimalField(
+ max_digits=7, decimal_places=4,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
@@ -2551,8 +2552,8 @@ class OrderPosition(AbstractPosition):
max_digits=13, decimal_places=2, null=True, blank=True,
)
- tax_rate = models.DecimalField(
- max_digits=7, decimal_places=2,
+ tax_rate = NormalizedDecimalField(
+ max_digits=7, decimal_places=4,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
@@ -3070,8 +3071,8 @@ class Transaction(models.Model):
price_includes_rounding_correction = models.DecimalField(
max_digits=13, decimal_places=2, default=Decimal("0.00")
)
- tax_rate = models.DecimalField(
- max_digits=7, decimal_places=2,
+ tax_rate = NormalizedDecimalField(
+ max_digits=7, decimal_places=4,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
@@ -3186,8 +3187,8 @@ class CartPosition(AbstractPosition):
verbose_name=_("Limit for extending expiration date"),
null=True
)
- tax_rate = models.DecimalField(
- max_digits=7, decimal_places=2, default=Decimal('0.00'),
+ tax_rate = NormalizedDecimalField(
+ max_digits=7, decimal_places=4, default=Decimal('0'),
verbose_name=_('Tax rate')
)
tax_code = models.CharField(
diff --git a/src/pretix/base/models/tax.py b/src/pretix/base/models/tax.py
index 70872d5bbc..4dc8120515 100644
--- a/src/pretix/base/models/tax.py
+++ b/src/pretix/base/models/tax.py
@@ -40,6 +40,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from pretix.base.templatetags.money import money_filter
from pretix.helpers.countries import FastCountryField
+from pretix.helpers.models import NormalizedDecimalField
class TaxedPrice:
@@ -335,9 +336,9 @@ class TaxRule(LoggedModel):
max_length=190,
choices=TAX_CODE_LISTS,
)
- rate = models.DecimalField(
- max_digits=10,
- decimal_places=2,
+ rate = NormalizedDecimalField(
+ max_digits=7,
+ decimal_places=4,
validators=[
MaxValueValidator(
limit_value=Decimal("100.00"),
diff --git a/src/pretix/base/templatetags/money.py b/src/pretix/base/templatetags/money.py
index e45468411d..bb3f674e73 100644
--- a/src/pretix/base/templatetags/money.py
+++ b/src/pretix/base/templatetags/money.py
@@ -26,6 +26,8 @@ from babel.numbers import format_currency
from django import template
from django.conf import settings
from django.template.defaultfilters import floatformat
+from django.utils import formats
+from django.utils.safestring import mark_safe
from pretix.base.i18n import get_babel_locale
@@ -82,3 +84,19 @@ def money_numberfield_filter(value: Decimal, arg=''):
places = settings.CURRENCY_PLACES.get(arg, 2)
return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP))
+
+
+@register.filter(is_safe=True)
+def tax_rate_format(number):
+ """
+ Display a Decimal to its significant decimal places, used for tax rates.
+ """
+ assert isinstance(number, Decimal)
+ return mark_safe(
+ formats.number_format(
+ number.normalize(),
+ -number.as_tuple().exponent,
+ use_l10n=True,
+ force_grouping=False,
+ )
+ )
diff --git a/src/pretix/control/templates/pretixcontrol/items/index.html b/src/pretix/control/templates/pretixcontrol/items/index.html
index 08d39ffa5f..16a2e8832a 100644
--- a/src/pretix/control/templates/pretixcontrol/items/index.html
+++ b/src/pretix/control/templates/pretixcontrol/items/index.html
@@ -156,11 +156,11 @@
{% if not i.tax_rule.price_includes_tax %}
- {% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name %}
+ {% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name %}
plus {{ rate }}% {{ taxname }}
{% endblocktrans %}
{% else %}
- {% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
{% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html
index 37e6171c6f..3528777611 100644
--- a/src/pretix/control/templates/pretixcontrol/order/index.html
+++ b/src/pretix/control/templates/pretixcontrol/order/index.html
@@ -705,7 +705,7 @@
{% if line.tax_rate %}
- {% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
plus {{ rate }}% {{ taxname }}
{% endblocktrans %}
@@ -715,7 +715,7 @@
{% if line.tax_rate and line.price %}
- {% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
@@ -755,7 +755,7 @@
{% if fee.tax_rate %}
- {% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
plus {{ rate }}% {{ taxname }}
{% endblocktrans %}
@@ -765,7 +765,7 @@
{% if fee.tax_rate %}
- {% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
diff --git a/src/pretix/helpers/models.py b/src/pretix/helpers/models.py
index 8e38141969..1a2bf93460 100644
--- a/src/pretix/helpers/models.py
+++ b/src/pretix/helpers/models.py
@@ -20,9 +20,11 @@
# .
#
import copy
+from decimal import Decimal
from django.core.files import File
from django.db import models
+from django.db.models.fields import DecimalField
class Thumbnail(models.Model):
@@ -54,3 +56,33 @@ def flatten_choices(choices):
yield from label_or_nested
else:
yield value_or_group, label_or_nested
+
+
+def _normalize_decimal(d: Decimal) -> Decimal:
+ """
+ Strips trailing zeros, e.g.
+
+ 20.000 → 20
+ 20.010 → 20.01
+ 20.100 → 20.1
+
+ But unlike of Decimal.normalize(), 20.000 will not become 2e+1. Very small decimals might still be represented
+ in scientific notation when printed.
+ """
+ normalized = d.normalize()
+ sign, digit, exponent = normalized.as_tuple()
+ if exponent > 0:
+ return normalized.quantize(1)
+ return normalized
+
+
+class NormalizedDecimalField(DecimalField):
+ """
+ Variant of DecimalField that never outputs the trailing zeros, so we always have normalized decimals internally.
+ Use this only for fields where the trailing zeros are pointless (e.g. percentages), not for monetary amounts.
+ """
+
+ def from_db_value(self, value, expression, connection):
+ if value is not None:
+ value = _normalize_decimal(value)
+ return value
diff --git a/src/pretix/plugins/reports/accountingreport.py b/src/pretix/plugins/reports/accountingreport.py
index 1af6b2b9e3..c086fc9594 100644
--- a/src/pretix/plugins/reports/accountingreport.py
+++ b/src/pretix/plugins/reports/accountingreport.py
@@ -28,7 +28,7 @@ from decimal import Decimal
from django import forms
from django.db.models import F, Sum
from django.db.models.functions import Coalesce
-from django.utils.formats import date_format, localize
+from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
@@ -43,7 +43,7 @@ from pretix.base.exporter import BaseExporter
from pretix.base.models import (
GiftCardTransaction, OrderFee, OrderPayment, OrderRefund, Transaction,
)
-from pretix.base.templatetags.money import money_filter
+from pretix.base.templatetags.money import money_filter, tax_rate_format
from pretix.base.timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
@@ -382,7 +382,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
else "",
tstyle_right,
),
- PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
+ PlainTextParagraph(tax_rate_format(r["tax_rate"]) + " %", tstyle_right),
PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
@@ -408,7 +408,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
[
PlainTextParagraph(_("Sum"), tstyle),
PlainTextParagraph("", tstyle_right),
- PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
+ PlainTextParagraph(tax_rate_format(tax_rate) + " %", tstyle_right),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(
money_filter(
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html
index 634765fe29..489b29772f 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html
@@ -173,11 +173,11 @@
{% trans "incl. taxes" %}
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
- {% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
+ {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
plus {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif var.display_price.rate and var.display_price.gross %}
- {% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
+ {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
{% endif %}
@@ -313,11 +313,11 @@
{% trans "incl. taxes" %}
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
- {% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
+ {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
plus {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif item.display_price.rate and item.display_price.gross %}
- {% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
+ {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
{% endif %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
index b0ae79bdf5..92f388a521 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
@@ -357,7 +357,7 @@
{% if line.tax_rate and line.total %}
- {% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
plus {{ rate }}% {{ taxname }}
{% endblocktrans %}
@@ -367,7 +367,7 @@
{% if line.tax_rate and line.total %}
- {% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
@@ -416,7 +416,7 @@
{% if fee.tax_rate %}
- {% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
plus {{ rate }}% {{ taxname }}
{% endblocktrans %}
@@ -426,7 +426,7 @@
{% if fee.tax_rate %}
- {% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
+ {% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html
index febcba9eb5..13daae66c3 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html
@@ -207,13 +207,13 @@
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
- {% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
+ {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
plus {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif var.display_price.rate and var.display_price.gross %}
- {% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
+ {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
@@ -372,13 +372,13 @@
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
- {% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
+ {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
plus {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif item.display_price.rate and item.display_price.gross %}
- {% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
+ {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/voucher.html b/src/pretix/presale/templates/pretixpresale/event/voucher.html
index 317f2be7f9..72913c6a6b 100644
--- a/src/pretix/presale/templates/pretixpresale/event/voucher.html
+++ b/src/pretix/presale/templates/pretixpresale/event/voucher.html
@@ -201,13 +201,13 @@
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
- {% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
+ {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
plus {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif var.display_price.rate and var.display_price.gross %}
- {% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
+ {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
@@ -356,13 +356,13 @@
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
- {% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
+ {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
plus {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif item.display_price.rate and item.display_price.gross %}
- {% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
+ {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
diff --git a/src/tests/helpers/test_models.py b/src/tests/helpers/test_models.py
new file mode 100644
index 0000000000..1995baadbb
--- /dev/null
+++ b/src/tests/helpers/test_models.py
@@ -0,0 +1,32 @@
+#
+# This file is part of pretix (Community Edition).
+#
+# Copyright (C) 2014-2020 Raphael Michel and contributors
+# Copyright (C) 2020-today pretix GmbH and contributors
+#
+# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
+# Public License as published by the Free Software Foundation in version 3 of the License.
+#
+# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
+# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
+# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
+# this file, see .
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
+# .
+#
+from decimal import Decimal
+
+from pretix.helpers.models import _normalize_decimal
+
+
+def test_normalize_decimal():
+ assert str(_normalize_decimal(Decimal("20.0000"))) == "20"
+ assert str(_normalize_decimal(Decimal("20.0001"))) == "20.0001"
+ assert str(_normalize_decimal(Decimal("20.0100"))) == "20.01"
+ assert str(_normalize_decimal(Decimal("2000000.0000"))) == "2000000"
+ assert str(_normalize_decimal(Decimal("0.0001"))) == "0.0001"
diff --git a/src/tests/presale/test_widget.py b/src/tests/presale/test_widget.py
index fbdbd35ca3..10795cb154 100644
--- a/src/tests/presale/test_widget.py
+++ b/src/tests/presale/test_widget.py
@@ -189,8 +189,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
- "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
- "suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
+ "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
+ "suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
"has_variations": False,
@@ -226,9 +226,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_red.pk,
'original_price': None,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
- "rate": "19.00", "includes_mixed_tax_rate": False},
+ "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
- "rate": "19.00", "includes_mixed_tax_rate": False},
+ "rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -239,9 +239,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_blue.pk,
'original_price': None,
"price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
- "rate": "19.00", "includes_mixed_tax_rate": False},
+ "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
- "rate": "19.00", "includes_mixed_tax_rate": False},
+ "rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -278,9 +278,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
- "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
+ "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
"includes_mixed_tax_rate": False},
- "suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
+ "suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
"includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
@@ -343,9 +343,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_red.pk,
'original_price': None,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
- "rate": "19.00", "includes_mixed_tax_rate": False},
+ "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
- "rate": "19.00", "includes_mixed_tax_rate": False},
+ "rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -395,8 +395,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
- "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
- "suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
+ "price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
+ "suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
"has_variations": False,
@@ -481,7 +481,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'gross': '14.00',
'net': '11.76',
'tax': '2.24',
- 'rate': '19.00',
+ 'rate': '19',
'name': '',
'includes_mixed_tax_rate': False
},
@@ -489,7 +489,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'gross': '14.00',
'net': '11.76',
'tax': '2.24',
- 'rate': '19.00',
+ 'rate': '19',
'name': '',
'includes_mixed_tax_rate': False
},
@@ -601,7 +601,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"net": "19.52",
"tax": "3.48",
"name": "MIXED!",
- "rate": "19.00",
+ "rate": "19",
"includes_mixed_tax_rate": True
}