Compare commits

..

6 Commits

Author SHA1 Message Date
Raphael Michel
f1e26040b2 Add uncommitted tests 2026-05-05 11:35:59 +02:00
Raphael Michel
32e6fed1d1 More tax rate handling 2026-04-29 10:08:20 +02:00
Raphael Michel
9d40df9310 Fix normalization 2026-04-29 09:57:29 +02:00
Raphael Michel
476a88d4fd Allow more decimal places for tax rates 2026-04-29 09:27:30 +02:00
Raphael Michel
bda27d72e7 Bump version to 2026.5.0.dev0 2026-04-28 16:48:33 +02:00
Raphael Michel
f67690bc56 Bump version to 2025.5.0.dev0 2026-04-28 16:47:51 +02:00
23 changed files with 230 additions and 63 deletions

View File

@@ -31,7 +31,6 @@ RUN apt-get update && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
chmod 0755 /pretix && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static && \
mkdir /etc/supervisord

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.4.1"
__version__ = "2026.5.0.dev0"

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
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

View File

@@ -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

View File

@@ -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'

View File

@@ -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',
@@ -1897,6 +1900,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
@@ -1980,6 +1984,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

View File

@@ -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', '0298_pluggable_permissions'),
]
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),
),
]

View File

@@ -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)

View File

@@ -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 (
@@ -2334,8 +2335,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(
@@ -2533,8 +2534,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(
@@ -3052,8 +3053,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(
@@ -3168,8 +3169,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(

View File

@@ -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"),

View File

@@ -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,
)
)

View File

@@ -156,11 +156,11 @@
<br/>
<small class="text-muted">
{% 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 %}
<strong>plus</strong> {{ 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 %}

View File

@@ -670,7 +670,7 @@
{% if line.tax_rate %}
<br/>
<small>
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -680,7 +680,7 @@
{% if line.tax_rate and line.price %}
<br/>
<small>
{% 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 %}
</small>
@@ -720,7 +720,7 @@
{% if fee.tax_rate %}
<br/>
<small>
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -730,7 +730,7 @@
{% if fee.tax_rate %}
<br/>
<small>
{% 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 %}
</small>

View File

@@ -20,9 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
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

View File

@@ -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,
),
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
Paragraph(tax_rate_format(r["tax_rate"]) + " %", tstyle_right),
Paragraph(str(r["sum_cont"]), tstyle_right),
Paragraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
@@ -408,7 +408,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
[
FontFallbackParagraph(_("Sum"), tstyle),
Paragraph("", tstyle_right),
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right),
Paragraph(tax_rate_format(tax_rate) + " %", tstyle_right),
Paragraph("", tstyle_right),
Paragraph(
money_filter(

View File

@@ -173,11 +173,11 @@
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
@@ -313,11 +313,11 @@
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}

View File

@@ -357,7 +357,7 @@
{% if line.tax_rate and line.total %}
<br />
<small>
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -367,7 +367,7 @@
{% if line.tax_rate and line.total %}
<br />
<small>
{% 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 %}
</small>
@@ -416,7 +416,7 @@
{% if fee.tax_rate %}
<br />
<small>
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -426,7 +426,7 @@
{% if fee.tax_rate %}
<br />
<small>
{% 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 %}
</small>

View File

@@ -207,13 +207,13 @@
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
</small>
@@ -372,13 +372,13 @@
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
</small>

View File

@@ -201,13 +201,13 @@
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
</small>
@@ -356,13 +356,13 @@
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% 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 %}
</small>

View File

@@ -265,7 +265,7 @@ EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
EMAIL_SUBJECT_PREFIX = '[pretix] '
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
EMAIL_TIMEOUT = 60
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]

View File

@@ -1,5 +1,4 @@
'use strict';
{
const globals = this;

View File

@@ -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 <https://pretix.eu/about/en/license>.
#
# 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
# <https://www.gnu.org/licenses/>.
#
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"

View File

@@ -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": 0,
@@ -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": 0,
@@ -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
}