From 3c3e59e932080dffe9dca18156693a8b6573a9dc Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 26 Feb 2018 10:46:07 +0100 Subject: [PATCH] Refs #99 -- Improve support for currencies with less than 2 decimal places (#783) * Refs #99 -- Fix stripe support for zero-decimal currencies * Add new money formatting method * Force decimal places in many places * Locale-aware currency rendering * Fix currencies in more places * More currency fixes --- src/pretix/base/decimal.py | 9 ++- src/pretix/base/forms/__init__.py | 14 ++--- src/pretix/base/i18n.py | 14 +++++ src/pretix/base/invoice.py | 17 +++--- src/pretix/base/models/invoices.py | 1 + src/pretix/base/models/tax.py | 14 ++++- src/pretix/base/models/vouchers.py | 14 +++-- src/pretix/base/notifications.py | 5 +- src/pretix/base/payment.py | 23 ++++++-- src/pretix/base/services/orders.py | 5 +- src/pretix/base/services/pricing.py | 5 ++ src/pretix/base/settings.py | 2 +- src/pretix/base/templatetags/money.py | 55 +++++++++++++++++++ src/pretix/control/forms/event.py | 8 +-- src/pretix/control/forms/item.py | 25 ++++++++- src/pretix/control/forms/orders.py | 21 ++++--- src/pretix/control/forms/subevents.py | 17 ++++-- src/pretix/control/logdisplay.py | 49 +++++++---------- .../templates/pretixcontrol/order/index.html | 15 ++--- .../templates/pretixcontrol/orders/index.html | 3 +- .../pretixcontrol/orders/overview.html | 48 ++++++++-------- .../pretixcontrol/search/orders.html | 3 +- .../pretixcontrol/subevents/detail.html | 2 +- .../pretixcontrol/waitinglist/index.html | 5 +- .../control/templatetags/order_overview.py | 11 +++- src/pretix/control/views/event.py | 10 +++- src/pretix/helpers/money.py | 34 ++++++++++++ .../banktransfer/email/order_pending.txt | 4 +- .../pretixplugins/banktransfer/pending.html | 3 +- .../banktransfer/transaction_list.html | 3 +- src/pretix/plugins/banktransfer/views.py | 5 +- src/pretix/plugins/checkinlists/exporters.py | 5 +- src/pretix/plugins/paypal/payment.py | 4 ++ src/pretix/plugins/reports/exporters.py | 52 +++++++++--------- src/pretix/plugins/stripe/payment.py | 21 ++++--- .../plugins/ticketoutputpdf/ticketoutput.py | 5 +- src/pretix/presale/forms/checkout.py | 13 +++-- .../pretixpresale/event/checkout_payment.html | 3 +- .../pretixpresale/event/fragment_cart.html | 23 ++++---- .../templates/pretixpresale/event/index.html | 23 ++++---- .../pretixpresale/event/order_pay_change.html | 3 +- .../event/order_pay_confirm.html | 5 +- .../pretixpresale/event/voucher.html | 15 ++--- src/pretix/settings.py | 18 ++++++ src/requirements/production.txt | 1 + src/setup.py | 1 + src/tests/plugins/stripe/test_provider.py | 30 ++++++++++ src/tests/presale/test_checkout.py | 10 ++-- src/tests/presale/test_orders.py | 2 +- 49 files changed, 467 insertions(+), 211 deletions(-) create mode 100644 src/pretix/base/templatetags/money.py create mode 100644 src/pretix/helpers/money.py diff --git a/src/pretix/base/decimal.py b/src/pretix/base/decimal.py index 2e6ede00ef..883a08a251 100644 --- a/src/pretix/base/decimal.py +++ b/src/pretix/base/decimal.py @@ -1,5 +1,12 @@ from decimal import ROUND_HALF_UP, Decimal +from django.conf import settings -def round_decimal(dec): + +def round_decimal(dec, currency=None): + if currency: + places = settings.CURRENCY_PLACES.get(currency, 2) + return Decimal(dec).quantize( + Decimal('1') / 10 ** places, ROUND_HALF_UP + ) return Decimal(dec).quantize(Decimal('0.01'), ROUND_HALF_UP) diff --git a/src/pretix/base/forms/__init__.py b/src/pretix/base/forms/__init__.py index 0af8d1d8b7..1c15aa368d 100644 --- a/src/pretix/base/forms/__init__.py +++ b/src/pretix/base/forms/__init__.py @@ -11,16 +11,16 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from .validators import PlaceholderValidator # NOQA -logger = logging.getLogger('pretix.plugins.ticketoutputpdf') +logger = logging.getLogger(__name__) class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm): # compatibility shim for django-i18nfield library def __init__(self, *args, **kwargs): - event = kwargs.pop('event', None) - if event: - kwargs['locales'] = event.settings.get('locales') + self.event = kwargs.pop('event', None) + if self.event: + kwargs['locales'] = self.event.settings.get('locales') super().__init__(*args, **kwargs) @@ -32,9 +32,9 @@ class I18nFormSet(i18nfield.forms.I18nModelFormSet): # compatibility shim for django-i18nfield library def __init__(self, *args, **kwargs): - event = kwargs.pop('event', None) - if event: - kwargs['locales'] = event.settings.get('locales') + self.event = kwargs.pop('event', None) + if self.event: + kwargs['locales'] = self.event.settings.get('locales') super().__init__(*args, **kwargs) diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py index f39935a6da..083115d610 100644 --- a/src/pretix/base/i18n.py +++ b/src/pretix/base/i18n.py @@ -12,6 +12,8 @@ from i18nfield.forms import I18nFormField # noqa from i18nfield.strings import LazyI18nString # noqa from i18nfield.utils import I18nJSONEncoder # noqa +from pretix.base.templatetags.money import money_filter + class LazyDate: def __init__(self, value): @@ -24,6 +26,18 @@ class LazyDate: return date_format(self.value, "SHORT_DATE_FORMAT") +class LazyCurrencyNumber: + def __init__(self, value, currency): + self.value = value + self.currency = currency + + def __format__(self, format_spec): + return self.__str__() + + def __str__(self): + return money_filter(self.value, self.currency) + + class LazyNumber: def __init__(self, value, decimal_pos=2): self.value = value diff --git a/src/pretix/base/invoice.py b/src/pretix/base/invoice.py index adb0e0ef66..5790b42208 100644 --- a/src/pretix/base/invoice.py +++ b/src/pretix/base/invoice.py @@ -24,6 +24,7 @@ from reportlab.platypus import ( from pretix.base.decimal import round_decimal from pretix.base.models import Event, Invoice from pretix.base.signals import register_invoice_renderers +from pretix.base.templatetags.money import money_filter class BaseInvoiceRenderer: @@ -376,14 +377,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): Paragraph(line.description, self.stylesheet['Normal']), "1", localize(line.tax_rate) + " %", - localize(line.net_value) + " " + self.invoice.event.currency, - localize(line.gross_value) + " " + self.invoice.event.currency, + money_filter(line.net_value, self.invoice.event.currency), + money_filter(line.gross_value, self.invoice.event.currency), )) else: tdata.append(( Paragraph(line.description, self.stylesheet['Normal']), "1", - localize(line.gross_value) + " " + self.invoice.event.currency, + money_filter(line.gross_value, self.invoice.event.currency), )) taxvalue_map[line.tax_rate, line.tax_name] += line.tax_value grossvalue_map[line.tax_rate, line.tax_name] += line.gross_value @@ -391,12 +392,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): if has_taxes: tdata.append([ - pgettext('invoice', 'Invoice total'), '', '', '', localize(total) + " " + self.invoice.event.currency + pgettext('invoice', 'Invoice total'), '', '', '', money_filter(total, self.invoice.event.currency) ]) colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)] else: tdata.append([ - pgettext('invoice', 'Invoice total'), '', localize(total) + " " + self.invoice.event.currency + pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency) ]) colwidths = [a * doc.width for a in (.65, .05, .30)] @@ -436,9 +437,9 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): tax = taxvalue_map[idx] tdata.append([ localize(rate) + " % " + name, - localize(gross - tax) + " " + self.invoice.event.currency, - localize(gross) + " " + self.invoice.event.currency, - localize(tax) + " " + self.invoice.event.currency, + money_filter(gross - tax, self.invoice.event.currency), + money_filter(gross, self.invoice.event.currency), + money_filter(tax, self.invoice.event.currency), '' ]) diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py index 249d3e0ccf..9cb53a4937 100644 --- a/src/pretix/base/models/invoices.py +++ b/src/pretix/base/models/invoices.py @@ -83,6 +83,7 @@ class Invoice(models.Model): foreign_currency_display = models.CharField(max_length=50, null=True, blank=True) foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True) foreign_currency_rate_date = models.DateField(null=True, blank=True) + file = models.FileField(null=True, blank=True, upload_to=invoice_filename) internal_reference = models.TextField(blank=True) diff --git a/src/pretix/base/models/tax.py b/src/pretix/base/models/tax.py index addb305779..349abf6a40 100644 --- a/src/pretix/base/models/tax.py +++ b/src/pretix/base/models/tax.py @@ -8,6 +8,7 @@ from i18nfield.fields import I18nCharField from pretix.base.decimal import round_decimal from pretix.base.models.base import LoggedModel +from pretix.base.templatetags.money import money_filter class TaxedPrice: @@ -23,6 +24,13 @@ class TaxedPrice: def __repr__(self): return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross)) + def print(self, currency): + return '{} + {}% = {}'.format( + money_filter(self.net, currency), + localize(self.rate), + money_filter(self.gross, currency) + ) + TAXED_ZERO = TaxedPrice( gross=Decimal('0.00'), @@ -129,10 +137,12 @@ class TaxRule(LoggedModel): if base_price_is == 'gross': gross = base_price - net = gross - round_decimal(base_price * (1 - 100 / (100 + self.rate))) + net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))), + self.event.currency if self.event else None) elif base_price_is == 'net': net = base_price - gross = round_decimal(net * (1 + self.rate / 100)) + gross = round_decimal((net * (1 + self.rate / 100)), + self.event.currency if self.event else None) else: raise ValueError('Unknown base price type: {}'.format(base_price_is)) diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 2d0942674c..390c6d8390 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -1,4 +1,4 @@ -from decimal import Decimal +from decimal import ROUND_HALF_UP, Decimal from django.conf import settings from django.core.exceptions import ValidationError @@ -368,9 +368,15 @@ class Voucher(LoggedModel): """ if self.value is not None: if self.price_mode == 'set': - return self.value + p = self.value elif self.price_mode == 'subtract': - return max(original_price - self.value, Decimal('0.00')) + p = max(original_price - self.value, Decimal('0.00')) elif self.price_mode == 'percent': - return round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00')) + p = round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00')) + else: + p = original_price + places = settings.CURRENCY_PLACES.get(self.event.currency, 2) + if places < 2: + return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP) + return p return original_price diff --git a/src/pretix/base/notifications.py b/src/pretix/base/notifications.py index 8e937849e5..021d1b4d32 100644 --- a/src/pretix/base/notifications.py +++ b/src/pretix/base/notifications.py @@ -2,11 +2,12 @@ import logging from collections import OrderedDict, namedtuple from django.dispatch import receiver -from django.utils.formats import date_format, localize +from django.utils.formats import date_format from django.utils.translation import ugettext_lazy as _ from pretix.base.models import Event, LogEntry from pretix.base.signals import register_notification_types +from pretix.base.templatetags.money import money_filter from pretix.helpers.urls import build_absolute_uri logger = logging.getLogger(__name__) @@ -174,7 +175,7 @@ class ParametrizedOrderNotificationType(NotificationType): url=order_url ) n.add_attribute(_('Order code'), order.code) - n.add_attribute(_('Order total'), '{} {}'.format(localize(order.total), logentry.event.currency)) + n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency)) n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT')) n.add_attribute(_('Order status'), order.get_status_display()) n.add_attribute(_('Order positions'), str(order.positions.count())) diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index af9754098f..8a0b6aa1d6 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -1,10 +1,11 @@ import logging from collections import OrderedDict -from decimal import Decimal +from decimal import ROUND_HALF_UP, Decimal from typing import Any, Dict, Union import pytz from django import forms +from django.conf import settings from django.contrib import messages from django.dispatch import receiver from django.forms import Form @@ -15,11 +16,11 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from i18nfield.forms import I18nFormField, I18nTextarea from i18nfield.strings import LazyI18nString -from pretix.base.decimal import round_decimal from pretix.base.models import CartPosition, Event, Order, Quota from pretix.base.reldate import RelativeDateField, RelativeDateWrapper from pretix.base.settings import SettingsSandbox from pretix.base.signals import register_payment_providers +from pretix.helpers.money import DecimalTextInput from pretix.presale.views import get_cart_total from pretix.presale.views.cart import get_or_create_cart_id @@ -91,10 +92,15 @@ class BasePaymentProvider: fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0) fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0) fee_reverse_calc = self.settings.get('_fee_reverse_calc', as_type=bool, default=True) + places = settings.CURRENCY_PLACES.get(self.event.currency, 2) if fee_reverse_calc: - return round_decimal((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price) + return ((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price).quantize( + Decimal('1') / 10 ** places, ROUND_HALF_UP + ) else: - return round_decimal(price * fee_percent / 100) + fee_abs + return (price * fee_percent / 100 + fee_abs).quantize( + Decimal('1') / 10 ** places, ROUND_HALF_UP + ) @property def verbose_name(self) -> str: @@ -156,6 +162,7 @@ class BasePaymentProvider: .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default implementation. """ + places = settings.CURRENCY_PLACES.get(self.event.currency, 2) return OrderedDict([ ('_enabled', forms.BooleanField( @@ -166,7 +173,10 @@ class BasePaymentProvider: forms.DecimalField( label=_('Additional fee'), help_text=_('Absolute value'), - required=False + localize=True, + required=False, + decimal_places=places, + widget=DecimalTextInput(places=places) )), ('_fee_percent', forms.DecimalField( @@ -174,7 +184,8 @@ class BasePaymentProvider: help_text=_('Percentage of the order total. Note that this percentage will currently only ' 'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping ' 'fees, if there are any.'), - required=False + localize=True, + required=False, )), ('_availability_date', RelativeDateField( diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index d2ef97529b..8d77c38c74 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -17,7 +17,7 @@ from django.utils.timezone import make_aware, now from django.utils.translation import ugettext as _ from pretix.base.i18n import ( - LazyDate, LazyLocaleException, LazyNumber, language, + LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language, ) from pretix.base.models import ( CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota, @@ -504,6 +504,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d for fee in fees: fee.order = order fee._calculate_tax() + if not fee.tax_rule.pk: + fee.tax_rule = None # TODO: deprecate fee.save() OrderPosition.transform_cart_positions(positions, order) @@ -568,6 +570,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], email_context = { 'total': LazyNumber(order.total), 'currency': event.currency, + 'total_with_currency': LazyCurrencyNumber(order.total, event.currency), 'date': LazyDate(order.expires), 'event': event.name, 'url': build_absolute_uri(event, 'presale:event.order', kwargs={ diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py index ec7a7117d6..f63247924e 100644 --- a/src/pretix/base/services/pricing.py +++ b/src/pretix/base/services/pricing.py @@ -1,5 +1,6 @@ from decimal import Decimal +from pretix.base.decimal import round_decimal from pretix.base.models import ( AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher, ) @@ -59,4 +60,8 @@ def get_price(item: Item, variation: ItemVariation = None, price.gross = price.net price.name = '' + price.gross = round_decimal(price.gross, item.event.currency) + price.net = round_decimal(price.net, item.event.currency) + price.tax = price.gross - price.net + return price diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 62ce482b27..6215a5c7f5 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -276,7 +276,7 @@ Your {event} team""")) 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, we successfully received your order for {event} with a total value -of {total} {currency}. Please complete your payment before {date}. +of {total_with_currency}. Please complete your payment before {date}. {payment_info} diff --git a/src/pretix/base/templatetags/money.py b/src/pretix/base/templatetags/money.py new file mode 100644 index 0000000000..29edc6f98a --- /dev/null +++ b/src/pretix/base/templatetags/money.py @@ -0,0 +1,55 @@ +from decimal import ROUND_HALF_UP, Decimal + +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 translation + +register = template.Library() + + +@register.filter("money") +def money_filter(value: Decimal, arg='', hide_currency=False): + if isinstance(value, float) or isinstance(value, int): + value = Decimal(value) + if not isinstance(value, Decimal): + raise TypeError("Invalid data type passed to money filter: %r" % type(value)) + if not arg: + raise ValueError("No currency passed.") + + places = settings.CURRENCY_PLACES.get(arg, 2) + rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP) + if places < 2 and rounded != value: + places = 2 + if hide_currency: + return floatformat(value, places) + + try: + if 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! + return '{} {}'.format( + arg, + floatformat(value, 2) + ) + return format_currency(value, arg, locale=translation.get_language()) + except: + return '{} {}'.format( + arg, + floatformat(value, places) + ) + + +@register.filter("money_numberfield") +def money_numberfield_filter(value: Decimal, arg=''): + if isinstance(value, float) or isinstance(value, int): + value = Decimal(value) + if not isinstance(value, Decimal): + raise TypeError("Invalid data type passed to money filter: %r" % type(value)) + if not arg: + raise ValueError("No currency passed.") + + places = settings.CURRENCY_PLACES.get(arg, 2) + return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 649677e642..046909636d 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -666,10 +666,10 @@ class MailSettingsForm(SettingsForm): label=_("Text"), required=False, widget=I18nTextarea, - help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {payment_info}, {url}, " - "{invoice_name}, {invoice_company}"), - validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{payment_info}', - '{url}', '{invoice_name}', '{invoice_company}'])] + help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, " + "{payment_info}, {url}, {invoice_name}, {invoice_company}"), + validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}', + '{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])] ) mail_text_order_paid = I18nFormField( label=_("Text"), diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 4e72a955fc..20ff8eea23 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -17,6 +17,7 @@ from pretix.base.models import ( from pretix.base.models.items import ItemAddOn from pretix.control.forms import SplitDateTimePickerWidget from pretix.control.forms.widgets import Select2 +from pretix.helpers.money import change_decimal_field class CategoryForm(I18nModelForm): @@ -159,6 +160,7 @@ class ItemCreateForm(I18nModelForm): self.fields['category'].queryset = self.instance.event.categories.all() self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all() + change_decimal_field(self.fields['default_price'], self.instance.event.currency) self.fields['tax_rule'].empty_label = _('No taxation') self.fields['copy_from'] = forms.ModelChoiceField( label=_("Copy product information"), @@ -292,6 +294,7 @@ class ItemUpdateForm(I18nModelForm): 'over 65. This ticket includes access to all parts of the event, except the VIP ' 'area.' ) + change_decimal_field(self.fields['default_price'], self.event.currency) class Meta: model = Item @@ -345,8 +348,29 @@ class ItemVariationsFormSet(I18nFormSet): return False return form.cleaned_data.get(DELETION_FIELD_NAME, False) + def _construct_form(self, i, **kwargs): + kwargs['event'] = self.event + return super()._construct_form(i, **kwargs) + + @property + def empty_form(self): + self.is_valid() + form = self.form( + auto_id=self.auto_id, + prefix=self.add_prefix('__prefix__'), + empty_permitted=True, + locales=self.locales, + event=self.event + ) + self.add_fields(form, None) + return form + class ItemVariationForm(I18nModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + change_decimal_field(self.fields['default_price'], self.event.currency) + class Meta: model = ItemVariation localized_fields = '__all__' @@ -399,7 +423,6 @@ class ItemAddOnsFormSet(I18nFormSet): class ItemAddOnForm(I18nModelForm): def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event') super().__init__(*args, **kwargs) self.fields['addon_category'].queryset = self.event.categories.all() diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index bcd04c1119..652c155173 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -2,7 +2,6 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError from django.db import models -from django.utils.formats import localize from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ @@ -12,6 +11,8 @@ from pretix.base.models import ( ) from pretix.base.models.event import SubEvent from pretix.base.services.pricing import get_price +from pretix.base.templatetags.money import money_filter +from pretix.helpers.money import change_decimal_field class ExtendForm(I18nModelForm): @@ -76,8 +77,8 @@ class SubEventChoiceField(forms.ModelChoiceField): p = get_price(self.instance.item, self.instance.variation, voucher=self.instance.voucher, subevent=obj) - return '{} – {} ({} {})'.format(obj.name, obj.get_date_range_display(), - p, self.instance.order.event.currency) + return '{} – {} ({})'.format(obj.name, obj.get_date_range_display(), + money_filter(p, self.instance.order.event.currency)) class OtherOperationsForm(forms.Form): @@ -120,6 +121,7 @@ class OrderPositionAddForm(forms.Form): price = forms.DecimalField( required=False, max_digits=10, decimal_places=2, + localize=True, label=_('Gross price'), help_text=_("Including taxes, if any. Keep empty for the product's default price") ) @@ -149,10 +151,10 @@ class OrderPositionAddForm(forms.Form): for v in variations: p = get_price(i, v, invoice_address=ia) choices.append(('%d-%d' % (i.pk, v.pk), - '%s – %s (%s %s)' % (pname, v.value, p, order.event.currency))) + '%s – %s (%s)' % (pname, v.value, p.print(order.event.currency)))) else: p = get_price(i, invoice_address=ia) - choices.append((str(i.pk), '%s (%s %s)' % (pname, p, order.event.currency))) + choices.append((str(i.pk), '%s (%s)' % (pname, p.print(order.event.currency)))) self.fields['itemvar'].choices = choices if ItemAddOn.objects.filter(base_item__event=order.event).exists(): self.fields['addon_to'].queryset = order.positions.filter(addon_to__isnull=True).select_related( @@ -165,6 +167,7 @@ class OrderPositionAddForm(forms.Form): self.fields['subevent'].queryset = order.event.subevents.all() else: del self.fields['subevent'] + change_decimal_field(self.fields['price'], order.event.currency) class OrderPositionChangeForm(forms.Form): @@ -178,6 +181,7 @@ class OrderPositionChangeForm(forms.Form): price = forms.DecimalField( required=False, max_digits=10, decimal_places=2, + localize=True, label=_('New price (gross)') ) operation = forms.ChoiceField( @@ -236,14 +240,13 @@ class OrderPositionChangeForm(forms.Form): p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent, invoice_address=ia) choices.append(('%d-%d' % (i.pk, v.pk), - '%s – %s (%s %s)' % (pname, v.value, localize(p), - instance.order.event.currency))) + '%s – %s (%s)' % (pname, v.value, p.print(instance.order.event.currency)))) else: p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent, invoice_address=ia) - choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(p), - instance.order.event.currency))) + choices.append((str(i.pk), '%s (%s)' % (pname, p.print(instance.order.event.currency)))) self.fields['itemvar'].choices = choices + change_decimal_field(self.fields['price'], instance.order.event.currency) def clean(self): if self.cleaned_data.get('operation') == 'price' and not self.cleaned_data.get('price', '') != '': diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py index e857d8b5fe..a710a2bc0e 100644 --- a/src/pretix/control/forms/subevents.py +++ b/src/pretix/control/forms/subevents.py @@ -5,7 +5,9 @@ from i18nfield.forms import I18nInlineFormSet from pretix.base.forms import I18nModelForm from pretix.base.models.event import SubEvent, SubEventMetaValue from pretix.base.models.items import SubEventItem +from pretix.base.templatetags.money import money_filter from pretix.control.forms import SplitDateTimePickerWidget +from pretix.helpers.money import change_decimal_field class SubEventForm(I18nModelForm): @@ -49,32 +51,35 @@ class SubEventItemOrVariationFormMixin: self.item = kwargs.pop('item') self.variation = kwargs.pop('variation', None) super().__init__(*args, **kwargs) + change_decimal_field(self.fields['price'], self.item.event.currency) class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format( - self.item.default_price, self.item.event.currency - ) + self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True) self.fields['price'].label = str(self.item.name) class Meta: model = SubEventItem fields = ['price'] + widgets = { + 'price': forms.TextInput + } class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format( - self.variation.price, self.item.event.currency - ) + self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True) self.fields['price'].label = '{} – {}'.format(str(self.item.name), self.variation.value) class Meta: model = SubEventItem fields = ['price'] + widgets = { + 'price': forms.TextInput + } class QuotaFormSet(I18nInlineFormSet): diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 36cfed17bf..b5ac30ce86 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -4,7 +4,6 @@ from decimal import Decimal import dateutil.parser import pytz from django.dispatch import receiver -from django.utils import formats from django.utils.formats import date_format from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from i18nfield.strings import LazyI18nString @@ -13,6 +12,7 @@ from pretix.base.models import ( CheckinList, Event, ItemVariation, LogEntry, OrderPosition, ) from pretix.base.signals import logentry_display +from pretix.base.templatetags.money import money_filter OVERVIEW_BLACKLIST = [ 'pretix.plugins.sendmail.order.email.sent' @@ -30,42 +30,38 @@ def _display_order_changed(event: Event, logentry: LogEntry): new_item = str(event.items.get(pk=data['new_item'])) if data['new_variation']: new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation'])) - return text + ' ' + _('Position #{posid}: {old_item} ({old_price} {currency}) changed ' - 'to {new_item} ({new_price} {currency}).').format( + return text + ' ' + _('Position #{posid}: {old_item} ({old_price}) changed ' + 'to {new_item} ({new_price}).').format( posid=data.get('positionid', '?'), old_item=old_item, new_item=new_item, - old_price=formats.localize(Decimal(data['old_price'])), - new_price=formats.localize(Decimal(data['new_price'])), - currency=event.currency + old_price=money_filter(Decimal(data['old_price']), event.currency), + new_price=money_filter(Decimal(data['new_price']), event.currency), ) elif logentry.action_type == 'pretix.event.order.changed.subevent': old_se = str(event.subevents.get(pk=data['old_subevent'])) new_se = str(event.subevents.get(pk=data['new_subevent'])) - return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price} {currency}) changed ' - 'to "{new_event}" ({new_price} {currency}).').format( + return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price}) changed ' + 'to "{new_event}" ({new_price}).').format( posid=data.get('positionid', '?'), old_event=old_se, new_event=new_se, - old_price=formats.localize(Decimal(data['old_price'])), - new_price=formats.localize(Decimal(data['new_price'])), - currency=event.currency + old_price=money_filter(Decimal(data['old_price']), event.currency), + new_price=money_filter(Decimal(data['new_price']), event.currency), ) elif logentry.action_type == 'pretix.event.order.changed.price': - return text + ' ' + _('Price of position #{posid} changed from {old_price} {currency} ' - 'to {new_price} {currency}.').format( + return text + ' ' + _('Price of position #{posid} changed from {old_price} ' + 'to {new_price}.').format( posid=data.get('positionid', '?'), - old_price=formats.localize(Decimal(data['old_price'])), - new_price=formats.localize(Decimal(data['new_price'])), - currency=event.currency + old_price=money_filter(Decimal(data['old_price']), event.currency), + new_price=money_filter(Decimal(data['new_price']), event.currency), ) elif logentry.action_type == 'pretix.event.order.changed.cancel': old_item = str(event.items.get(pk=data['old_item'])) if data['old_variation']: old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation'])) - return text + ' ' + _('Position #{posid} ({old_item}, {old_price} {currency}) removed.').format( + return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) removed.').format( posid=data.get('positionid', '?'), old_item=old_item, - old_price=formats.localize(Decimal(data['old_price'])), - currency=event.currency + old_price=money_filter(Decimal(data['old_price']), event.currency), ) elif logentry.action_type == 'pretix.event.order.changed.add': item = str(event.items.get(pk=data['item'])) @@ -73,30 +69,27 @@ def _display_order_changed(event: Event, logentry: LogEntry): item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation'])) if data['addon_to']: addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to']) - return text + ' ' + _('Position #{posid} created: {item} ({price} {currency}) as an add-on to ' + return text + ' ' + _('Position #{posid} created: {item} ({price}) as an add-on to ' 'position #{addon_to}.').format( posid=data.get('positionid', '?'), item=item, addon_to=addon_to.positionid, - price=formats.localize(Decimal(data['price'])), - currency=event.currency + price=money_filter(Decimal(data['price']), event.currency), ) else: - return text + ' ' + _('Position #{posid} created: {item} ({price} {currency}).').format( + return text + ' ' + _('Position #{posid} created: {item} ({price}).').format( posid=data.get('positionid', '?'), item=item, - price=formats.localize(Decimal(data['price'])), - currency=event.currency + price=money_filter(Decimal(data['price']), event.currency), ) elif logentry.action_type == 'pretix.event.order.changed.split': old_item = str(event.items.get(pk=data['old_item'])) if data['old_variation']: old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation'])) - return text + ' ' + _('Position #{posid} ({old_item}, {old_price} {currency}) split into new order: {order}').format( + return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format( old_item=old_item, posid=data.get('positionid', '?'), order=data['new_order'], - old_price=formats.localize(Decimal(data['old_price'])), - currency=event.currency + old_price=money_filter(Decimal(data['old_price']), event.currency), ) elif logentry.action_type == 'pretix.event.order.changed.split_from': return _('This order has been created by splitting the order {order}').format( diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index fd73aed5c0..3bed87e0c0 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -2,6 +2,7 @@ {% load i18n %} {% load bootstrap3 %} {% load eventurl %} +{% load money %} {% load safelink %} {% load eventsignal %} {% block title %} @@ -257,7 +258,7 @@
{% if event.settings.display_net_prices %} - {{ event.currency }} {{ line.net_price|floatformat:2 }} + {{ line.net_price|money:event.currency }} {% if line.tax_rate %}
@@ -267,7 +268,7 @@ {% endif %} {% else %} - {{ event.currency }} {{ line.price|floatformat:2 }} + {{ line.price|money:event.currency }} {% if line.tax_rate and line.price %}
@@ -291,7 +292,7 @@
{% if event.settings.display_net_prices %} - {{ event.currency }} {{ fee.net_value|floatformat:2 }} + {{ fee.net_value|money:event.currency }} {% if fee.tax_rate %}
@@ -301,7 +302,7 @@ {% endif %} {% else %} - {{ event.currency }} {{ fee.value|floatformat:2 }} + {{ fee.value|money:event.currency }} {% if fee.tax_rate %}
@@ -321,7 +322,7 @@ {% trans "Net total" %}
- {{ event.currency }} {{ items.net_total|floatformat:2 }} + {{ items.net_total|money:event.currency }}
@@ -330,7 +331,7 @@ {% trans "Taxes" %}
- {{ event.currency }} {{ items.tax_total|floatformat:2 }} + {{ items.tax_total|money:event.currency }}
@@ -340,7 +341,7 @@ {% trans "Total" %}
- {{ event.currency }} {{ items.total|floatformat:2 }} + {{ items.total|money:event.currency }}
diff --git a/src/pretix/control/templates/pretixcontrol/orders/index.html b/src/pretix/control/templates/pretixcontrol/orders/index.html index 6a91e784e1..e8a023981c 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/index.html +++ b/src/pretix/control/templates/pretixcontrol/orders/index.html @@ -2,6 +2,7 @@ {% load i18n %} {% load eventurl %} {% load urlreplace %} +{% load money %} {% load bootstrap3 %} {% block title %}{% trans "Orders" %}{% endblock %} {% block content %} @@ -114,7 +115,7 @@ {% endif %} {{ o.datetime|date:"SHORT_DATETIME_FORMAT" }} - {{ o.total|floatformat:2 }} {{ request.event.currency }} + {{ o.total|money:request.event.currency }} {{ o.pcnt }} {% include "pretixcontrol/orders/fragment_order_status.html" with order=o %} diff --git a/src/pretix/control/templates/pretixcontrol/orders/overview.html b/src/pretix/control/templates/pretixcontrol/orders/overview.html index 6224dccdb6..a607ee1878 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/overview.html +++ b/src/pretix/control/templates/pretixcontrol/orders/overview.html @@ -50,12 +50,12 @@ {% if tup.0 %} {{ tup.0.name }} - {{ tup.0.num_canceled|togglesum }} - {{ tup.0.num_refunded|togglesum }} - {{ tup.0.num_expired|togglesum }} - {{ tup.0.num_pending|togglesum }} - {{ tup.0.num_paid|togglesum }} - {{ tup.0.num_total|togglesum }} + {{ tup.0.num_canceled|togglesum:request.event.currency }} + {{ tup.0.num_refunded|togglesum:request.event.currency }} + {{ tup.0.num_expired|togglesum:request.event.currency }} + {{ tup.0.num_pending|togglesum:request.event.currency }} + {{ tup.0.num_paid|togglesum:request.event.currency }} + {{ tup.0.num_total|togglesum:request.event.currency }} {% endif %} {% for item in tup.1 %} @@ -63,43 +63,43 @@ {{ item.name }} - {{ item.num_canceled|togglesum }} + {{ item.num_canceled|togglesum:request.event.currency }} - {{ item.num_refunded|togglesum }} + {{ item.num_refunded|togglesum:request.event.currency }} - {{ item.num_expired|togglesum }} + {{ item.num_expired|togglesum:request.event.currency }} - {{ item.num_pending|togglesum }} + {{ item.num_pending|togglesum:request.event.currency }} - {{ item.num_paid|togglesum }} + {{ item.num_paid|togglesum:request.event.currency }} - {{ item.num_total|togglesum }} + {{ item.num_total|togglesum:request.event.currency }} {% if item.has_variations %} {% for var in item.all_variations %} {{ var }} - {{ var.num_canceled|togglesum }} - {{ var.num_refunded|togglesum }} - {{ var.num_expired|togglesum }} - {{ var.num_pending|togglesum }} - {{ var.num_paid|togglesum }} - {{ var.num_total|togglesum }} + {{ var.num_canceled|togglesum:request.event.currency }} + {{ var.num_refunded|togglesum:request.event.currency }} + {{ var.num_expired|togglesum:request.event.currency }} + {{ var.num_pending|togglesum:request.event.currency }} + {{ var.num_paid|togglesum:request.event.currency }} + {{ var.num_total|togglesum:request.event.currency }} {% endfor %} {% endif %} @@ -109,12 +109,12 @@ {% trans "Total" %} - {{ total.num_canceled|togglesum }} - {{ total.num_refunded|togglesum }} - {{ total.num_expired|togglesum }} - {{ total.num_pending|togglesum }} - {{ total.num_paid|togglesum }} - {{ total.num_total|togglesum }} + {{ total.num_canceled|togglesum:request.event.currency }} + {{ total.num_refunded|togglesum:request.event.currency }} + {{ total.num_expired|togglesum:request.event.currency }} + {{ total.num_pending|togglesum:request.event.currency }} + {{ total.num_paid|togglesum:request.event.currency }} + {{ total.num_total|togglesum:request.event.currency }} diff --git a/src/pretix/control/templates/pretixcontrol/search/orders.html b/src/pretix/control/templates/pretixcontrol/search/orders.html index 64db07e2dc..01eb551d85 100644 --- a/src/pretix/control/templates/pretixcontrol/search/orders.html +++ b/src/pretix/control/templates/pretixcontrol/search/orders.html @@ -2,6 +2,7 @@ {% load i18n %} {% load eventurl %} {% load urlreplace %} +{% load money %} {% load bootstrap3 %} {% block title %}{% trans "Order search" %}{% endblock %} {% block content %} @@ -70,7 +71,7 @@ {% endif %} {{ o.datetime|date:"SHORT_DATETIME_FORMAT" }} - {{ o.total|floatformat:2 }} {{ o.event.currency }} + {{ o.total|money:o.event.currency }} {% include "pretixcontrol/orders/fragment_order_status.html" with order=o %} {% empty %} diff --git a/src/pretix/control/templates/pretixcontrol/subevents/detail.html b/src/pretix/control/templates/pretixcontrol/subevents/detail.html index db8d05a936..1499c6904d 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/detail.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/detail.html @@ -120,7 +120,7 @@
{% trans "Item prices" %} {% for f in itemvar_forms %} - {% bootstrap_field f.price layout="control" %} + {% bootstrap_field f.price addon_after=request.event.currency layout="control" %} {% endfor %}
diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index 02d9eddf86..10da3e325c 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -1,6 +1,7 @@ {% extends "pretixcontrol/event/base.html" %} {% load i18n %} {% load eventurl %} +{% load money %} {% load urlreplace %} {% block title %}{% trans "Waiting list" %}{% endblock %} {% block content %} @@ -65,9 +66,9 @@ {% trans "Sales estimate" %}
- {% blocktrans trimmed with amount=estimate|default:0|floatformat:2 currency=request.event.currency %} + {% blocktrans trimmed with amount=estimate|default:0|money:request.event.currency %} If you can make enough room at your event to fit all the persons on the waiting list in, you - could sell tickets worth an additional {{ amount }} {{ currency }}. + could sell tickets worth an additional {{ amount }}. {% endblocktrans %}
diff --git a/src/pretix/control/templatetags/order_overview.py b/src/pretix/control/templatetags/order_overview.py index c4e4f21aef..d7167700b5 100644 --- a/src/pretix/control/templatetags/order_overview.py +++ b/src/pretix/control/templatetags/order_overview.py @@ -1,5 +1,6 @@ from django import template -from django.utils import formats +from django.conf import settings +from django.template.defaultfilters import floatformat from django.utils.html import conditional_escape from django.utils.safestring import mark_safe @@ -7,7 +8,7 @@ register = template.Library() @register.filter(name='togglesum', needs_autoescape=True) -def cut(value, autoescape=True): +def togglesum_filter(value, arg='EUR', autoescape=True): def noop(x): return x @@ -17,6 +18,10 @@ def cut(value, autoescape=True): esc = conditional_escape else: esc = noop + + places = settings.CURRENCY_PLACES.get(arg, 2) return mark_safe('{0}{1}{2}'.format( - esc(value[0]), esc(formats.localize(value[1])), esc(formats.localize(value[2])) + esc(value[0]), + esc(floatformat(value[1], places)), + esc(floatformat(value[2], places)) )) diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 4483674057..23c073818c 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -1,6 +1,7 @@ import re from collections import OrderedDict from datetime import timedelta +from decimal import Decimal from urllib.parse import urlsplit from django.conf import settings @@ -26,6 +27,7 @@ from django.views.generic.detail import SingleObjectMixin from i18nfield.strings import LazyI18nString from pytz import timezone +from pretix.base.i18n import LazyCurrencyNumber from pretix.base.models import ( CachedCombinedTicket, CachedTicket, Event, Item, ItemVariation, LogEntry, Order, RequiredAction, TaxRule, Voucher, @@ -34,6 +36,7 @@ from pretix.base.models.event import EventMetaValue from pretix.base.services import tickets from pretix.base.services.invoices import build_preview_invoice_pdf from pretix.base.signals import event_live_issues, register_ticket_outputs +from pretix.base.templatetags.money import money_filter from pretix.control.forms.event import ( CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm, EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm, @@ -492,8 +495,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View): return { 'date': date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'), 'expire_date': date_format(now() + timedelta(days=15), 'SHORT_DATE_FORMAT'), - 'payment_info': _('{} {} has been transferred to account <9999-9999-9999-9999> at {}').format( - 42.23, self.request.event.currency, date_format(now(), 'SHORT_DATETIME_FORMAT')) + 'payment_info': _('{} has been transferred to account <9999-9999-9999-9999> at {}').format( + money_filter(Decimal('42.23'), self.request.event.currency), date_format(now(), 'SHORT_DATETIME_FORMAT')) } # create index-language mapping @@ -508,7 +511,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View): @cached_property def items(self): return { - 'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company', + 'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company', 'total_with_currency', 'event', 'payment_info', 'url', 'invoice_name'], 'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'], 'mail_text_order_free': ['event', 'url', 'invoice_name', 'invoice_company'], @@ -536,6 +539,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View): return { 'event': self.request.event.name, 'total': 42.23, + 'total_with_currency': LazyCurrencyNumber(42.23, self.request.event.currency), 'currency': self.request.event.currency, 'url': self.generate_order_url(user_orders[0]['code'], user_orders[0]['secret']), 'orders': '\n'.join(orders), diff --git a/src/pretix/helpers/money.py b/src/pretix/helpers/money.py new file mode 100644 index 0000000000..4845300bac --- /dev/null +++ b/src/pretix/helpers/money.py @@ -0,0 +1,34 @@ +from decimal import Decimal + +from django.conf import settings +from django.core.validators import DecimalValidator +from django.forms import NumberInput, TextInput +from django.utils import formats + + +class DecimalTextInput(TextInput): + def __init__(self, *args, **kwargs): + self.places = kwargs.pop('places', 2) + super().__init__(*args, **kwargs) + + def format_value(self, value): + """ + Return a value as it should appear when rendered in a template. + """ + if value == '' or value is None: + return None + if isinstance(value, str): + return value + return formats.localize_input(value.quantize(Decimal('1') / 10 ** self.places)) + + +def change_decimal_field(field, currency): + places = settings.CURRENCY_PLACES.get(currency, 2) + field.decimal_places = places + if isinstance(field.widget, NumberInput): + field.widget.attrs['step'] = str(Decimal('1') / 10 ** places).lower() + elif isinstance(field.widget, TextInput): + field.widget = DecimalTextInput(places=places) + v = [v for v in field.validators if isinstance(v, DecimalValidator)] + if len(v) == 1: + v[0].decimal_places = places diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/email/order_pending.txt b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/email/order_pending.txt index 271bc13778..644a9e048c 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/email/order_pending.txt +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/email/order_pending.txt @@ -1,7 +1,7 @@ -{% load i18n %}{% load l10n %}{% blocktrans with bank=details|safe code=order.full_code total=order.total|localize currency=event.currency %} +{% load i18n %}{% load l10n %}{% load money %}{% blocktrans with bank=details|safe code=order.full_code total=order.total|money:event.currency %} Please transfer the full amount to the following bank account. Reference: {{ code }} - Amount: {{ total }} {{ currency }} + Amount: {{ total }} {{ bank }} {% endblocktrans %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html index 2c6624c4cc..1264cfd584 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html @@ -1,5 +1,6 @@ {% load i18n %} {% load l10n %} +{% load money %}

{% blocktrans trimmed %} Please transfer the full amount to the following bank account: @@ -7,6 +8,6 @@

{{ details|linebreaksbr }}
- {% trans "Amount:" %} {{ order.total|localize }} {{ event.currency }}
+ {% trans "Amount:" %} {{ order.total|money:event.currency }}
{% trans "Reference code (important):" %} {{ order.full_code }}
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html index a0f9dea422..9db62d3a60 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html @@ -1,5 +1,6 @@ {% load i18n %} {% load rich_text %} +{% load money %} {% load staticfiles %}
{% csrf_token %} @@ -85,7 +86,7 @@ {% if trans.order %} + data-toggle="tooltip" title="{{ trans.order.total|money:trans.order.event.currency }}"> {% if not request.event %} {{ trans.order.event.slug|upper }}-{{ trans.order.code }} {% else %} diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index 7ad260cc75..62411076ad 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -18,6 +18,7 @@ from pretix.base.models import Order, Quota from pretix.base.services.mail import SendMailException from pretix.base.services.orders import mark_order_paid from pretix.base.settings import SettingsSandbox +from pretix.base.templatetags.money import money_filter from pretix.control.permissions import ( EventPermissionRequiredMixin, OrganizerPermissionRequiredMixin, ) @@ -147,8 +148,6 @@ class ActionView(View): }) def get(self, request, *args, **kwargs): - from django.utils.formats import localize - u = request.GET.get('query', '') if len(u) < 2: return JsonResponse({'results': []}) @@ -178,7 +177,7 @@ class ActionView(View): { 'code': o.event.slug.upper() + '-' + o.code, 'status': o.get_status_display(), - 'total': localize(o.total) + ' ' + o.event.currency + 'total': money_filter(o.total, o.event.currency) } for o in qs ] }) diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py index 576ea8ba42..c0a6c70ea7 100644 --- a/src/pretix/plugins/checkinlists/exporters.py +++ b/src/pretix/plugins/checkinlists/exporters.py @@ -6,7 +6,7 @@ from defusedcsv import csv from django import forms from django.db.models import Max, OuterRef, Subquery 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.timezone import is_aware, make_aware from django.utils.translation import pgettext, ugettext as _, ugettext_lazy from pytz import UTC @@ -15,6 +15,7 @@ from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle from pretix.base.exporter import BaseExporter from pretix.base.models import Checkin, Order, OrderPosition, Question +from pretix.base.templatetags.money import money_filter from pretix.plugins.reports.exporters import ReportlabExportMixin @@ -200,7 +201,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList): op.order.code, name, str(op.item.name) + (" – " + str(op.variation.value) if op.variation else "") + "\n" + - self.event.currency + " " + localize(op.price), + money_filter(op.price, self.event.currency), ] acache = {} for a in op.answers.all(): diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py index 673b1d43d3..f10150c2a7 100644 --- a/src/pretix/plugins/paypal/payment.py +++ b/src/pretix/plugins/paypal/payment.py @@ -55,6 +55,8 @@ class Paypal(BasePaymentProvider): ('client_id', forms.CharField( label=_('Client ID'), + max_length=80, + min_length=80, help_text=_('{text}').format( text=_('Click here for a tutorial on how to obtain the required keys'), docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html' @@ -63,6 +65,8 @@ class Paypal(BasePaymentProvider): ('secret', forms.CharField( label=_('Secret'), + max_length=80, + min_length=80, )) ] ) diff --git a/src/pretix/plugins/reports/exporters.py b/src/pretix/plugins/reports/exporters.py index 3ca53274bd..1ee8eded8d 100644 --- a/src/pretix/plugins/reports/exporters.py +++ b/src/pretix/plugins/reports/exporters.py @@ -7,7 +7,8 @@ from django import forms from django.conf import settings from django.contrib.staticfiles import finders from django.db.models import Sum -from django.utils.formats import date_format, localize +from django.template.defaultfilters import floatformat +from django.utils.formats import date_format from django.utils.timezone import get_current_timezone, now from django.utils.translation import pgettext, pgettext_lazy, ugettext as _ @@ -194,49 +195,50 @@ class OverviewReport(Report): ] items_by_category, total = order_overview(self.event, subevent=self.form_data.get('subevent')) + places = settings.CURRENCY_PLACES.get(self.event.currency, 2) for tup in items_by_category: if tup[0]: tstyledata.append(('FONTNAME', (0, len(tdata)), (-1, len(tdata)), 'OpenSansBd')) tdata.append([ tup[0].name, - str(tup[0].num_canceled[0]), localize(tup[0].num_canceled[1]), - str(tup[0].num_refunded[0]), localize(tup[0].num_refunded[1]), - str(tup[0].num_expired[0]), localize(tup[0].num_expired[1]), - str(tup[0].num_pending[0]), localize(tup[0].num_pending[1]), - str(tup[0].num_paid[0]), localize(tup[0].num_paid[1]), - str(tup[0].num_total[0]), localize(tup[0].num_total[1]), + str(tup[0].num_canceled[0]), floatformat(tup[0].num_canceled[1], places), + str(tup[0].num_refunded[0]), floatformat(tup[0].num_refunded[1], places), + str(tup[0].num_expired[0]), floatformat(tup[0].num_expired[1], places), + str(tup[0].num_pending[0]), floatformat(tup[0].num_pending[1], places), + str(tup[0].num_paid[0]), floatformat(tup[0].num_paid[1], places), + str(tup[0].num_total[0]), floatformat(tup[0].num_total[1], places), ]) for item in tup[1]: tdata.append([ " " + str(item.name), - str(item.num_canceled[0]), localize(item.num_canceled[1]), - str(item.num_refunded[0]), localize(item.num_refunded[1]), - str(item.num_expired[0]), localize(item.num_expired[1]), - str(item.num_pending[0]), localize(item.num_pending[1]), - str(item.num_paid[0]), localize(item.num_paid[1]), - str(item.num_total[0]), localize(item.num_total[1]), + str(item.num_canceled[0]), floatformat(item.num_canceled[1], places), + str(item.num_refunded[0]), floatformat(item.num_refunded[1], places), + str(item.num_expired[0]), floatformat(item.num_expired[1], places), + str(item.num_pending[0]), floatformat(item.num_pending[1], places), + str(item.num_paid[0]), floatformat(item.num_paid[1], places), + str(item.num_total[0]), floatformat(item.num_total[1], places), ]) if item.has_variations: for var in item.all_variations: tdata.append([ " " + str(var), - str(var.num_canceled[0]), localize(var.num_canceled[1]), - str(var.num_refunded[0]), localize(var.num_refunded[1]), - str(var.num_expired[0]), localize(var.num_expired[1]), - str(var.num_pending[0]), localize(var.num_pending[1]), - str(var.num_paid[0]), localize(var.num_paid[1]), - str(var.num_total[0]), localize(var.num_total[1]), + str(var.num_canceled[0]), floatformat(var.num_canceled[1], places), + str(var.num_refunded[0]), floatformat(var.num_refunded[1], places), + str(var.num_expired[0]), floatformat(var.num_expired[1], places), + str(var.num_pending[0]), floatformat(var.num_pending[1], places), + str(var.num_paid[0]), floatformat(var.num_paid[1], places), + str(var.num_total[0]), floatformat(var.num_total[1], places), ]) tdata.append([ _("Total"), - str(total['num_canceled'][0]), localize(total['num_canceled'][1]), - str(total['num_refunded'][0]), localize(total['num_refunded'][1]), - str(total['num_expired'][0]), localize(total['num_expired'][1]), - str(total['num_pending'][0]), localize(total['num_pending'][1]), - str(total['num_paid'][0]), localize(total['num_paid'][1]), - str(total['num_total'][0]), localize(total['num_total'][1]), + str(total['num_canceled'][0]), floatformat(total['num_canceled'][1], places), + str(total['num_refunded'][0]), floatformat(total['num_refunded'][1], places), + str(total['num_expired'][0]), floatformat(total['num_expired'][1], places), + str(total['num_pending'][0]), floatformat(total['num_pending'][1], places), + str(total['num_paid'][0]), floatformat(total['num_paid'][1], places), + str(total['num_total'][0]), floatformat(total['num_total'][1], places), ]) table = Table(tdata, colWidths=colwidths, repeatRows=3) diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index ba76ae018f..046daff19d 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -5,6 +5,7 @@ from collections import OrderedDict import stripe from django import forms +from django.conf import settings from django.contrib import messages from django.template.loader import get_template from django.utils.translation import ugettext, ugettext_lazy as _ @@ -165,6 +166,10 @@ class StripeMethod(BasePaymentProvider): def order_prepare(self, request, order): return self.checkout_prepare(request, None) + def _get_amount(self, order): + places = settings.CURRENCY_PLACES.get(self.event.currency, 2) + return int(order.total * 10 ** places) + def _init_api(self): stripe.api_version = '2017-06-05' stripe.api_key = self.settings.get('secret_key') @@ -180,7 +185,7 @@ class StripeMethod(BasePaymentProvider): def _charge_source(self, request, source, order): try: charge = stripe.Charge.create( - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), source=source, metadata={ @@ -269,7 +274,7 @@ class StripeMethod(BasePaymentProvider): if order.payment_info: payment_info = json.loads(order.payment_info) if 'amount' in payment_info: - payment_info['amount'] /= 100 + payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2) else: payment_info = None template = get_template('pretixplugins/stripe/control.html') @@ -411,7 +416,7 @@ class StripeCC(StripeMethod): request.session['payment_stripe_order_secret'] = order.secret source = stripe.Source.create( type='three_d_secure', - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), three_d_secure={ 'card': src.id @@ -479,7 +484,7 @@ class StripeGiropay(StripeMethod): try: source = stripe.Source.create( type='giropay', - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), metadata={ 'order': str(order.id), @@ -538,7 +543,7 @@ class StripeIdeal(StripeMethod): def _create_source(self, request, order): source = stripe.Source.create( type='ideal', - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), metadata={ 'order': str(order.id), @@ -585,7 +590,7 @@ class StripeAlipay(StripeMethod): def _create_source(self, request, order): source = stripe.Source.create( type='alipay', - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), metadata={ 'order': str(order.id), @@ -634,7 +639,7 @@ class StripeBancontact(StripeMethod): try: source = stripe.Source.create( type='bancontact', - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), metadata={ 'order': str(order.id), @@ -706,7 +711,7 @@ class StripeSofort(StripeMethod): def _create_source(self, request, order): source = stripe.Source.create( type='sofort', - amount=int(order.total * 100), + amount=self._get_amount(order), currency=self.event.currency.lower(), metadata={ 'order': str(order.id), diff --git a/src/pretix/plugins/ticketoutputpdf/ticketoutput.py b/src/pretix/plugins/ticketoutputpdf/ticketoutput.py index 6607a19f0b..358f47145f 100644 --- a/src/pretix/plugins/ticketoutputpdf/ticketoutput.py +++ b/src/pretix/plugins/ticketoutputpdf/ticketoutput.py @@ -11,7 +11,7 @@ from django.core.files import File from django.core.files.storage import default_storage from django.http import HttpRequest from django.template.loader import get_template -from django.utils.formats import date_format, localize +from django.utils.formats import date_format from django.utils.translation import ugettext_lazy as _ from pytz import timezone from reportlab.graphics import renderPDF @@ -29,6 +29,7 @@ from reportlab.platypus import Paragraph from pretix.base.i18n import language from pretix.base.models import Order, OrderPosition +from pretix.base.templatetags.money import money_filter from pretix.base.ticketoutput import BaseTicketOutput from pretix.plugins.ticketoutputpdf.signals import ( get_fonts, layout_text_variables, @@ -81,7 +82,7 @@ DEFAULT_VARIABLES = OrderedDict(( ("price", { "label": _("Price"), "editor_sample": _("123.45 EUR"), - "evaluate": lambda op, order, event: '{} {}'.format(event.currency, localize(op.price)) + "evaluate": lambda op, order, event: money_filter(op.price, event.currency) }), ("attendee_name", { "label": _("Attendee name"), diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index 672cdc63e1..198305addb 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -13,6 +13,7 @@ from pretix.base.forms.questions import ( ) from pretix.base.models import ItemVariation from pretix.base.models.tax import TAXED_ZERO +from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.rich_text import rich_text from pretix.base.validators import EmailBlacklistValidator from pretix.presale.signals import contact_form_fields @@ -133,17 +134,17 @@ class AddOnsForm(forms.Form): name=label ) elif not price.rate: - n = _('{name} (+ {currency} {price})').format( - name=label, currency=event.currency, price=number_format(price.gross) + n = _('{name} (+ {price})').format( + name=label, price=money_filter(price.gross, event.currency) ) elif event.settings.display_net_prices: - n = _('{name} (+ {currency} {price} plus {taxes}% {taxname})').format( - name=label, currency=event.currency, price=number_format(price.net), + n = _('{name} (+ {price} plus {taxes}% {taxname})').format( + name=label, price=money_filter(price.net, event.currency), taxes=number_format(price.rate), taxname=price.name ) else: - n = _('{name} (+ {currency} {price} incl. {taxes}% {taxname})').format( - name=label, currency=event.currency, price=number_format(price.gross), + n = _('{name} (+ {price} incl. {taxes}% {taxname})').format( + name=label, price=money_filter(price.gross, event.currency), taxes=number_format(price.rate), taxname=price.name ) diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html index 0a9335cfcc..eabacb5b1c 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html @@ -1,5 +1,6 @@ {% extends "pretixpresale/event/checkout_base.html" %} {% load i18n %} +{% load money %} {% load bootstrap3 %} {% block inner %}

{% trans "Please select how you want to pay." %}

@@ -12,7 +13,7 @@

{% if show_fees %} - {% if p.fee < 0 %}-{% else %}+{% endif %} {{ p.fee|floatformat:2|cut:"-" }} {{ event.currency }} + {% if p.fee < 0 %}-{% else %}+{% endif %} {{ p.fee|money:event.currency|cut:"-" }} {% endif %} @@ -71,9 +72,9 @@
 
{% if event.settings.display_net_prices %} - {{ event.currency }} {{ line.net_price|floatformat:2 }} + {{ line.net_price|money:event.currency }} {% else %} - {{ event.currency }} {{ line.price|floatformat:2 }} + {{ line.price|money:event.currency }} {% endif %}
{% else %} @@ -113,15 +114,15 @@

{% if event.settings.display_net_prices %} - {{ event.currency }} {{ line.net_price|floatformat:2 }} + {{ line.net_price|money:event.currency }} {% else %} - {{ event.currency }} {{ line.price|floatformat:2 }} + {{ line.price|money:event.currency }} {% endif %}
{% endif %}
{% if event.settings.display_net_prices %} - {{ event.currency }} {{ line.net_total|floatformat:2 }} + {{ line.net_total|money:event.currency }} {% if line.tax_rate and line.total %}
@@ -131,7 +132,7 @@ {% endif %} {% else %} - {{ event.currency }} {{ line.total|floatformat:2 }} + {{ line.total|money:event.currency }} {% if line.tax_rate and line.total %}
@@ -165,7 +166,7 @@
{% if event.settings.display_net_prices %} - {{ event.currency }} {{ fee.net_value|floatformat:2 }} + {{ fee.net_value|money:event.currency }} {% if fee.tax_rate %}
@@ -175,7 +176,7 @@ {% endif %} {% else %} - {{ event.currency }} {{ fee.value|floatformat:2 }} + {{ fee.value|money:event.currency }} {% if fee.tax_rate %}
@@ -195,7 +196,7 @@ {% trans "Net total" %}
- {{ event.currency }} {{ cart.net_total|floatformat:2 }} + {{ cart.net_total|money:event.currency }}
@@ -204,7 +205,7 @@ {% trans "Taxes" %}
- {{ event.currency }} {{ cart.tax_total|floatformat:2 }} + {{ cart.tax_total|money:event.currency }}
@@ -214,7 +215,7 @@ {% trans "Total" %}
- {{ event.currency }} {{ cart.total|floatformat:2 }} + {{ cart.total|money:event.currency }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html index cdc10f3f51..372d2b2808 100644 --- a/src/pretix/presale/templates/pretixpresale/event/index.html +++ b/src/pretix/presale/templates/pretixpresale/event/index.html @@ -2,6 +2,7 @@ {% load i18n %} {% load l10n %} {% load eventurl %} +{% load money %} {% load thumbnail %} {% load eventsignal %} {% load rich_text %} @@ -250,13 +251,13 @@
{% if item.min_price != item.max_price or item.free_price %} - {% blocktrans trimmed with minprice=item.min_price|floatformat:2 currency=event.currency %} - from {{ currency }} {{ minprice }} + {% blocktrans trimmed with minprice=item.min_price|money:event.currency %} + from {{ minprice }} {% endblocktrans %} {% elif not item.min_price and not item.max_price %} {% trans "FREE" context "price" %} {% else %} - {{ event.currency }} {{ item.min_price|floatformat:2 }} + {{ item.min_price|money:event.currency }} {% endif %}
@@ -288,18 +289,18 @@ {{ event.currency }}
{% elif not var.display_price.gross %} {% trans "FREE" context "price" %} {% elif event.settings.display_net_prices %} - {{ event.currency }} {{ var.display_price.net|floatformat:2 }} + {{ var.display_price.net|money:event.currency }} {% else %} - {{ event.currency }} {{ var.display_price.gross|floatformat:2 }} + {{ var.display_price.gross|money:event.currency }} {% endif %} {% if var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %} {% blocktrans trimmed with rate=var.display_price.rate name=var.display_price.name %} @@ -377,17 +378,17 @@
{{ event.currency }}
{% elif not item.display_price.gross %} {% trans "FREE" context "price" %} {% elif event.settings.display_net_prices %} - {{ event.currency }} {{ item.display_price.net|floatformat:2 }} + {{ item.display_price.net|money:event.currency }} {% else %} - {{ event.currency }} {{ item.display_price.gross|floatformat:2 }} + {{ item.display_price.gross|money:event.currency }} {% endif %} {% if item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %} {% blocktrans trimmed with rate=item.display_price.rate name=item.display_price.name %} diff --git a/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html b/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html index 04d1d8b334..ebe0970e59 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html @@ -1,6 +1,7 @@ {% extends "pretixpresale/event/base.html" %} {% load i18n %} {% load eventurl %} +{% load money %} {% block title %}{% trans "Change payment method" %}{% endblock %} {% block content %}

@@ -22,7 +23,7 @@