diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index dc4479252a..56225077d1 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -11,6 +11,7 @@ from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from pretix.base.decimal import round_decimal from pretix.base.i18n import I18nCharField, I18nTextField from pretix.base.models.base import LoggedModel @@ -221,6 +222,11 @@ class Item(LoggedModel): if self.event: self.event.get_cache().clear() + @property + def default_price_net(self): + tax_value = round_decimal(self.default_price * (1 - 100 / (100 + self.tax_rate))) + return self.default_price - tax_value + def is_available(self, now_dt: datetime=None) -> bool: """ Returns whether this item is available according to its ``active`` flag @@ -313,6 +319,11 @@ class ItemVariation(models.Model): def price(self): return self.default_price if self.default_price is not None else self.item.default_price + @property + def net_price(self): + tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate))) + return self.price - tax_value + def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.item: diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 6096aaf897..17eb093b01 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -7,10 +7,11 @@ from typing import List, Union from django.conf import settings from django.db import models -from django.db.models import F +from django.db.models import F, Sum from django.db.models.signals import post_delete from django.dispatch import receiver from django.utils.crypto import get_random_string +from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -215,6 +216,18 @@ class Order(LoggedModel): else: self.payment_fee_tax_value = Decimal('0.00') + @property + def payment_fee_net(self): + return self.payment_fee - self.payment_fee_tax_value + + @cached_property + def tax_total(self): + return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + self.payment_fee_tax_value + + @property + def net_total(self): + return self.total - self.tax_total + @staticmethod def normalize_code(code): tr = str.maketrans({ @@ -432,6 +445,10 @@ class AbstractPosition(models.Model): else: q.answer = "" + @property + def net_price(self): + return self.price - self.tax_value + class OrderPosition(AbstractPosition): """ diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 3536b98e25..bbf953e115 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -9,6 +9,7 @@ from django.db.models import Q from django.utils.timezone import now from django.utils.translation import ugettext as _ +from pretix.base.decimal import round_decimal from pretix.base.i18n import LazyLocaleException from pretix.base.models import ( CartPosition, Event, Item, ItemVariation, Voucher, @@ -143,6 +144,8 @@ class CartManager: custom_price = Decimal(custom_price.replace(",", ".")) if custom_price > 100000000: return error_messages['price_too_high'] + if self.event.settings.display_net_prices: + custom_price = round_decimal(custom_price * (100 + item.tax_rate) / 100) price = max(custom_price, price) return price diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 1d8756739f..5fa933b564 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -20,6 +20,10 @@ DEFAULTS = { 'default': '10', 'type': int }, + 'display_net_prices': { + 'default': 'False', + 'type': bool + }, 'attendee_names_asked': { 'default': 'True', 'type': bool diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index b2a091f5f0..4c39d2aae7 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -158,6 +158,12 @@ class EventSettingsForm(SettingsForm): help_text=_("Show item details before presale has started and after presale has ended"), required=False ) + display_net_prices = forms.BooleanField( + label=_("Show net prices instead of gross prices in the product list (not recommended!)"), + help_text=_("Independent of your choice, the cart will show gross prices as this the price that needs to be " + "paid"), + required=False + ) presale_start_show_date = forms.BooleanField( label=_("Show start date"), help_text=_("Show the presale start date before presale has started."), diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 10af48350c..4c70b3c8b2 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -59,7 +59,7 @@ class OrderPositionChangeForm(forms.Form): price = forms.DecimalField( required=False, max_digits=10, decimal_places=2, - label=_('New price') + label=_('New price (gross)') ) operation = forms.ChoiceField( required=False, diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index 6e4cdc760b..b48e57d63b 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -25,6 +25,7 @@ {% bootstrap_field sform.contact_mail layout="horizontal" %} {% bootstrap_field sform.imprint_url layout="horizontal" %} {% bootstrap_field sform.show_quota_left layout="horizontal" %} + {% bootstrap_field sform.display_net_prices layout="horizontal" %}
{% trans "Timeline" %} diff --git a/src/pretix/control/templates/pretixcontrol/order/change.html b/src/pretix/control/templates/pretixcontrol/order/change.html index f4549abf92..b67cc104ec 100644 --- a/src/pretix/control/templates/pretixcontrol/order/change.html +++ b/src/pretix/control/templates/pretixcontrol/order/change.html @@ -79,6 +79,9 @@ {% if position.form.operation.value == "price" %}checked="checked"{% endif %}> {% trans "Change price to" %} {% bootstrap_field position.form.price layout='inline' %} + {% if request.event.settings.display_net_prices %} + {% trans "Enter a gross price including taxes." %} + {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index c141504caa..9d6f899dd2 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -189,12 +189,20 @@ {% endif %}
- {{ event.currency }} {{ line.price|floatformat:2 }} - {% if line.tax_rate %} -
- {% blocktrans trimmed with rate=line.tax_rate %} + {% if event.settings.display_net_prices %} + {{ event.currency }} {{ line.net_price|floatformat:2 }} + {% if line.tax_rate %} +
{% blocktrans trimmed with rate=line.tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% endif %} + {% else %} + {{ event.currency }} {{ line.price|floatformat:2 }} + {% if line.tax_rate %} +
{% blocktrans trimmed with rate=line.tax_rate %} incl. {{ rate }}% taxes {% endblocktrans %} + {% endif %} {% endif %}
@@ -206,17 +214,47 @@ {% trans "Payment method fee" %}
- {{ event.currency }} {{ items.payment_fee|floatformat:2 }} - {% if order.payment_fee_tax_rate %} -
- {% blocktrans trimmed with rate=order.payment_fee_tax_rate %} - incl. {{ rate }}% taxes - {% endblocktrans %} + {% if event.settings.display_net_prices %} + {{ event.currency }} {{ order.payment_fee_net|floatformat:2 }} + {% if order.payment_fee_tax_rate %} +
+ {% blocktrans trimmed with rate=order.payment_fee_tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% endif %} + {% else %} + {{ event.currency }} {{ items.payment_fee|floatformat:2 }} + {% if order.payment_fee_tax_rate %} +
+ {% blocktrans trimmed with rate=order.payment_fee_tax_rate %} + incl. {{ rate }}% taxes + {% endblocktrans %} + {% endif %} {% endif %}
{% endif %} + {% if event.settings.display_net_prices %} +
+
+ {% trans "Net total" %} +
+
+ {{ event.currency }} {{ items.net_total|floatformat:2 }} +
+
+
+
+
+ {% trans "Taxes" %} +
+
+ {{ event.currency }} {{ items.tax_total|floatformat:2 }} +
+
+
+ {% endif %}
{% trans "Total" %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 5647125379..4e3bf58fd1 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -179,6 +179,8 @@ class OrderDetail(OrderView): 'raw': cartpos, 'total': self.object.total, 'payment_fee': self.object.payment_fee, + 'net_total': self.object.net_total, + 'tax_total': self.object.tax_total, } diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html index 93912e3625..1662defa06 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html @@ -70,22 +70,35 @@ + value="{% if event.settings.display_net_prices %}{{ line.net_price }}{% else %}{{ line.price }}{% endif %}" /> {% endif %} {% endif %}
- {{ event.currency }} {{ line.price|floatformat:2 }} + {% if event.settings.display_net_prices %} + {{ event.currency }} {{ line.net_price|floatformat:2 }} + {% else %} + {{ event.currency }} {{ line.price|floatformat:2 }} + {% endif %}
{% endif %}
- {{ event.currency }} {{ line.total|floatformat:2 }} - {% if line.tax_rate %} -
{% blocktrans trimmed with rate=line.tax_rate %} - incl. {{ rate }}% taxes - {% endblocktrans %} + {% if event.settings.display_net_prices %} + {{ event.currency }} {{ line.net_total|floatformat:2 }} + {% if line.tax_rate %} +
{% blocktrans trimmed with rate=line.tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% endif %} + {% else %} + {{ event.currency }} {{ line.total|floatformat:2 }} + {% if line.tax_rate %} +
{% blocktrans trimmed with rate=line.tax_rate %} + incl. {{ rate }}% taxes + {% endblocktrans %} + {% endif %} {% endif %}
{% if download %} @@ -102,23 +115,52 @@
{% endfor %} {% if cart.payment_fee %} - {# TODO: Tax rate? #}
{% trans "Payment method fee" %}
- {{ event.currency }} {{ cart.payment_fee|floatformat:2 }} - {% if cart.payment_fee_tax_rate %} -
- {% blocktrans trimmed with rate=cart.payment_fee_tax_rate %} - incl. {{ rate }}% taxes - {% endblocktrans %} + {% if event.settings.display_net_prices %} + {{ event.currency }} {{ cart.payment_fee_net|floatformat:2 }} + {% if cart.payment_fee_tax_rate %} +
+ {% blocktrans trimmed with rate=cart.payment_fee_tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% endif %} + {% else %} + {{ event.currency }} {{ cart.payment_fee|floatformat:2 }} + {% if cart.payment_fee_tax_rate %} +
+ {% blocktrans trimmed with rate=cart.payment_fee_tax_rate %} + incl. {{ rate }}% taxes + {% endblocktrans %} + {% endif %} {% endif %}
{% endif %} +{% if event.settings.display_net_prices %} +
+
+ {% trans "Net total" %} +
+
+ {{ event.currency }} {{ cart.net_total|floatformat:2 }} +
+
+
+
+
+ {% trans "Taxes" %} +
+
+ {{ event.currency }} {{ cart.tax_total|floatformat:2 }} +
+
+
+{% endif %}
{% trans "Total" %} diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html index 12e9149ecd..e661741c12 100644 --- a/src/pretix/presale/templates/pretixpresale/event/index.html +++ b/src/pretix/presale/templates/pretixpresale/event/index.html @@ -136,14 +136,18 @@ {{ event.currency }} + step="any" value="{{ var.display_price|stringformat:"0.2f" }}">
{% else %} - {{ event.currency }} {{ var.price|floatformat:2 }} + {{ event.currency }} {{ var.display_price|floatformat:2 }} {% endif %} - {% if item.tax_rate %} + {% if item.tax_rate and event.settings.display_net_prices %} + {% blocktrans trimmed with rate=item.tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% elif item.tax_rate %} {% blocktrans trimmed with rate=item.tax_rate %} incl. {{ rate }}% taxes {% endblocktrans %} @@ -186,14 +190,18 @@
{{ event.currency }} + step="any" value="{{ item.display_price|stringformat:"0.2f" }}">
{% else %} - {{ event.currency }} {{ item.price|floatformat:2 }} + {{ event.currency }} {{ item.display_price|floatformat:2 }} {% endif %} - {% if item.tax_rate %} + {% if item.tax_rate and event.settings.display_net_prices %} + {% blocktrans trimmed with rate=item.tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% elif item.tax_rate %} {% blocktrans trimmed with rate=item.tax_rate %} incl. {{ rate }}% taxes {% endblocktrans %} diff --git a/src/pretix/presale/templates/pretixpresale/event/voucher.html b/src/pretix/presale/templates/pretixpresale/event/voucher.html index 6ae4d67a47..4a2eaca0e1 100644 --- a/src/pretix/presale/templates/pretixpresale/event/voucher.html +++ b/src/pretix/presale/templates/pretixpresale/event/voucher.html @@ -72,7 +72,11 @@ {% else %} {{ event.currency }} {{ var.display_price|floatformat:2 }} {% endif %} - {% if item.tax_rate %} + {% if item.tax_rate and event.settings.display_net_prices %} + {% blocktrans trimmed with rate=item.tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% elif item.tax_rate %} {% blocktrans trimmed with rate=item.tax_rate %} incl. {{ rate }}% taxes {% endblocktrans %} @@ -126,7 +130,11 @@ {% else %} {{ event.currency }} {{ item.price|floatformat:2 }} {% endif %} - {% if item.tax_rate %} + {% if item.tax_rate and event.settings.display_net_prices %} + {% blocktrans trimmed with rate=item.tax_rate %} + plus {{ rate }}% taxes + {% endblocktrans %} + {% elif item.tax_rate %} {% blocktrans trimmed with rate=item.tax_rate %} incl. {{ rate }}% taxes {% endblocktrans %} diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index 7dae0beffe..a25d11821c 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -6,6 +6,7 @@ from django.db.models import Sum from django.utils.functional import cached_property from django.utils.timezone import now +from pretix.base.decimal import round_decimal from pretix.base.models import CartPosition, OrderPosition from pretix.base.signals import register_payment_providers @@ -44,10 +45,10 @@ class CartMixin: else: i = pos.pk if downloads: - return i, pos.pk, 0, 0, 0, 0 + return i, pos.pk, 0, 0, 0, 0, if answers and ((pos.item.admission and self.request.event.settings.attendee_names_asked) or pos.item.questions.all()): - return i, pos.pk, 0, 0, 0, 0 + return i, pos.pk, 0, 0, 0, 0, return 0, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0) positions = [] @@ -56,15 +57,24 @@ class CartMixin: group = g[0] group.count = len(g) group.total = group.count * group.price + group.net_total = group.count * group.net_price group.has_questions = answers and k[0] != "" if answers: group.cache_answers() positions.append(group) total = sum(p.total for p in positions) + net_total = sum(p.net_total for p in positions) + tax_total = sum(p.total - p.net_total for p in positions) payment_fee = payment_fee if payment_fee is not None else self.get_payment_fee(total) - payment_fee_tax_rate = payment_fee_tax_rate if payment_fee_tax_rate is not None else self.request.event.settings.tax_rate_default + payment_fee_tax_rate = round_decimal(payment_fee_tax_rate + if payment_fee_tax_rate is not None + else self.request.event.settings.tax_rate_default) + payment_fee_tax_value = round_decimal(payment_fee * (1 - 100 / (100 + payment_fee_tax_rate))) + payment_fee_net = payment_fee - payment_fee_tax_value + tax_total += payment_fee_tax_value + net_total += payment_fee_net try: first_expiry = min(p.expires for p in positions) if positions else now() @@ -77,7 +87,10 @@ class CartMixin: 'positions': positions, 'raw': cartpos, 'total': total + payment_fee, + 'net_total': net_total, + 'tax_total': tax_total, 'payment_fee': payment_fee, + 'payment_fee_net': payment_fee_net, 'payment_fee_tax_rate': payment_fee_tax_rate, 'answers': answers, 'minutes_left': minutes_left, diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 5464a7f32f..fba0100da1 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -6,6 +6,7 @@ from django.utils.timezone import now from django.utils.translation import ugettext as _ from django.views.generic import TemplateView, View +from pretix.base.decimal import round_decimal from pretix.base.models import CartPosition, Quota, Voucher from pretix.base.services.cart import ( CartError, add_items_to_cart, remove_items_from_cart, @@ -189,6 +190,8 @@ class RedeemView(EventViewMixin, TemplateView): else: item.cached_availability = item.check_quotas() item.price = self.voucher.calculate_price(item.default_price) + if self.request.event.settings.display_net_prices: + item.price -= round_decimal(item.price * (1 - 100 / (100 + item.tax_rate))) else: for var in item.available_variations: if self.voucher.allow_ignore_quota or self.voucher.block_quota: @@ -196,10 +199,12 @@ class RedeemView(EventViewMixin, TemplateView): else: var.cached_availability = list(var.check_quotas()) var.display_price = self.voucher.calculate_price(var.price) + if self.request.event.settings.display_net_prices: + var.display_price -= round_decimal(var.display_price * (1 - 100 / (100 + item.tax_rate))) if len(item.available_variations) > 0: - item.min_price = min([v.price for v in item.available_variations]) - item.max_price = max([v.price for v in item.available_variations]) + item.min_price = min([v.display_price for v in item.available_variations]) + item.max_price = max([v.display_price for v in item.available_variations]) items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations] context['options'] = sum([(len(item.available_variations) if item.has_variations else 1) diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index dba44d85a3..5a55bc82af 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -63,6 +63,7 @@ def get_grouped_items(event): if item.cached_availability[1] is not None else sys.maxsize, int(event.settings.max_items_per_order)) item.price = item.default_price + item.display_price = item.default_price_net if event.settings.display_net_prices else item.price display_add_to_cart = display_add_to_cart or item.order_max > 0 else: for var in item.available_variations: @@ -70,10 +71,11 @@ def get_grouped_items(event): var.order_max = min(var.cached_availability[1] if var.cached_availability[1] is not None else sys.maxsize, int(event.settings.max_items_per_order)) + var.display_price = var.net_price if event.settings.display_net_prices else var.price display_add_to_cart = display_add_to_cart or var.order_max > 0 if len(item.available_variations) > 0: - item.min_price = min([v.price for v in item.available_variations]) - item.max_price = max([v.price for v in item.available_variations]) + item.min_price = min([v.display_price for v in item.available_variations]) + item.max_price = max([v.display_price for v in item.available_variations]) items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations] return items, display_add_to_cart