Compare commits

..

5 Commits

Author SHA1 Message Date
Raphael Michel
b27a059ccb Update src/pretix/helpers/thumb.py
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-09-09 14:34:53 +02:00
Raphael Michel
b8dbda4eb4 Update src/pretix/helpers/thumb.py
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-09-09 14:34:32 +02:00
Raphael Michel
82d4a09da9 Fix note from review 2025-06-27 17:55:40 +02:00
Raphael Michel
9dc554dfa8 Python backwards compat 2025-06-27 17:14:15 +02:00
Raphael Michel
7b05af6bfc Provide high-res versions of product and event images
Replaces previous attempts #3235 and #5056, see also #3506
2025-06-27 17:14:14 +02:00
54 changed files with 557 additions and 916 deletions

View File

@@ -26,8 +26,6 @@ rate decimal (string) Tax rate in per
code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
default boolean If ``true`` (default), this is the default tax rate for this event
(there can only be one per event).
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
are applied. Will be ignored if custom rules are set.
Use custom rules instead.
@@ -50,10 +48,6 @@ custom_rules object Dynamic rules s
The ``code`` attribute has been added.
.. versionchanged:: 2025.4
The ``default`` attribute has been added.
.. _rest-taxcodes:
Tax codes
@@ -117,7 +111,6 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"default": true,
"internal_name": "VAT",
"code": "S/standard",
"rate": "19.00",
@@ -160,7 +153,6 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"default": true,
"internal_name": "VAT",
"code": "S/standard",
"rate": "19.00",
@@ -211,7 +203,6 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"default": false,
"internal_name": "VAT",
"code": "S/standard",
"rate": "19.00",

View File

@@ -685,26 +685,8 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
class Meta:
model = TaxRule
fields = ('id', 'name', 'default', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules', 'default')
def create(self, validated_data):
if "default" not in validated_data and not self.context["event"].tax_rules.exists():
validated_data["default"] = True
return super().create(validated_data)
def save(self, **kwargs):
if self.validated_data.get("default"):
if self.instance and self.instance.pk:
self.context["event"].tax_rules.exclude(pk=self.instance.pk).update(default=False)
else:
self.context["event"].tax_rules.update(default=False)
return super().save(**kwargs)
def validate_default(self, value):
if not value and self.instance.default:
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
return value
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
class EventSettingsSerializer(SettingsSerializer):
@@ -730,8 +712,6 @@ class EventSettingsSerializer(SettingsSerializer):
'allow_modifications_after_checkin',
'last_order_modification_date',
'show_quota_left',
'tax_rule_payment',
'tax_rule_cancellation',
'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours',
@@ -962,8 +942,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_random_uid',
'system_question_order',
'tax_rule_payment',
'tax_rule_cancellation',
]
def __init__(self, *args, **kwargs):

View File

@@ -580,11 +580,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
)
super().perform_destroy(instance)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx["event"] = self.request.event
return ctx
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = ItemMetaPropertiesSerializer

View File

@@ -1,24 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0281_event_is_remote"),
]
operations = [
migrations.AddField(
model_name="taxrule",
name="default",
field=models.BooleanField(default=False),
),
migrations.AddConstraint(
model_name="taxrule",
constraint=models.UniqueConstraint(
condition=models.Q(("default", True)),
fields=("event",),
name="one_default_per_event",
),
),
]

View File

@@ -1,60 +0,0 @@
# Generated by Django 4.2.17 on 2025-03-28 09:19
from django.core.cache import cache
from django.db import migrations, models
from django.db.models import Count, Exists, OuterRef
def set_default_tax_rate(app, schema_editor):
Event = app.get_model('pretixbase', 'Event')
Event_SettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
TaxRule = app.get_model('pretixbase', 'TaxRule')
# Handling of events with tax_rate_default set
for s in Event_SettingsStore.objects.filter(key="tax_rate_default").iterator():
updated = TaxRule.objects.filter(pk=s.value, event_id=s.object_id).update(default=True)
if updated:
# Delete deprecated settings key
s.delete()
# The default for new events is tax_rule_cancellation=none, but since we do not change behaviour
# for existing events without warning, we create a settings entry that matches the old behaviour.
Event_SettingsStore.objects.get_or_create(
object_id=s.object_id,
key="tax_rule_cancellation",
defaults={"value": "default"},
)
# We do not need to set tax_rule_payment here since "default" is the default
cache.delete('hierarkey_{}_{}'.format('event', s.object_id))
# Handling of events with tax_rate_default not set
for e in Event.objects.only("pk").exclude(Exists(TaxRule.objects.filter(default=True, event_id=OuterRef("pk")))).iterator():
fav_tax_rules = e.tax_rules.annotate(c=Count("item")).order_by("-c", "pk")[:1]
if fav_tax_rules:
fav_tax_rules[0].default = True
fav_tax_rules[0].save()
# Previously, no tax rule was set for payments, so keep it this way
Event_SettingsStore.objects.get_or_create(
object=e,
key="tax_rule_payment",
defaults={"value": "none"},
)
cache.delete('hierarkey_{}_{}'.format('event', e.pk))
# We do not need to set tax_rule_cancellation, as "none" is the new system default
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0282_taxrule_default"),
]
operations = [
migrations.RunPython(
set_default_tax_rate,
migrations.RunPython.noop,
),
]

View File

@@ -1113,6 +1113,13 @@ class Event(EventMixin, LoggedModel):
newname = default_storage.save(fname, fi)
s.value = 'file://' + newname
settings_to_save.append(s)
elif s.key == 'tax_rate_default':
try:
if int(s.value) in tax_map:
s.value = tax_map.get(int(s.value)).pk
settings_to_save.append(s)
except ValueError:
pass
elif s.key.startswith('payment_') and s.key.endswith('__restrict_to_sales_channels'):
data = other.settings._unserialize(s.value, as_type=list)
data = [ident for ident in data if ident in valid_sales_channel_identifers]
@@ -1191,10 +1198,6 @@ class Event(EventMixin, LoggedModel):
renderers[pp.identifier] = pp
return renderers
@cached_property
def cached_default_tax_rule(self):
return self.tax_rules.filter(default=True).first()
@cached_property
def ticket_secret_generators(self) -> dict:
"""

View File

@@ -2373,17 +2373,17 @@ class OrderFee(models.Model):
self.fee_type, self.value
)
def _calculate_tax(self, tax_rule=None, invoice_address=None):
def _calculate_tax(self, tax_rule=None):
if tax_rule:
self.tax_rule = tax_rule
try:
ia = invoice_address or self.order.invoice_address
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if not self.tax_rule and self.fee_type == "payment" and self.order.event.settings.tax_rule_payment == "default":
self.tax_rule = self.order.event.cached_default_tax_rule
if not self.tax_rule and self.fee_type == "payment" and self.order.event.settings.tax_rate_default:
self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule:
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True)

View File

@@ -377,20 +377,9 @@ class TaxRule(LoggedModel):
'if configured above.'),
)
custom_rules = models.TextField(blank=True, null=True)
default = models.BooleanField(
verbose_name=_('Default'),
default=False,
)
class Meta:
ordering = ('event', 'rate', 'id')
constraints = [
models.UniqueConstraint(
fields=["event"],
condition=models.Q(default=True),
name="one_default_per_event",
),
]
class SaleNotAllowed(Exception):
pass
@@ -405,7 +394,7 @@ class TaxRule(LoggedModel):
and not OrderFee.objects.filter(tax_rule=self, order__event=self.event).exists()
and not OrderPosition.all.filter(tax_rule=self, order__event=self.event).exists()
and not self.event.items.filter(tax_rule=self).exists()
and not (self.default and self.event.tax_rules.filter(~models.Q(pk=self.pk)).exists())
and self.event.settings.tax_rate_default != self
)
@classmethod

View File

@@ -32,7 +32,7 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
SubEvent, TaxRule, User, WaitingListEntry,
SubEvent, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, mail
@@ -40,7 +40,6 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
)
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.services.tax import split_fee_for_taxes
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
@@ -269,33 +268,13 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
if fee:
tax_rule_zero = TaxRule.zero()
if event.settings.tax_rule_cancellation == "default":
fee_values = [(event.cached_default_tax_rule or tax_rule_zero, fee)]
elif event.settings.tax_rule_cancellation == "split":
fee_values = split_fee_for_taxes(positions, fee, event)
else:
fee_values = [(tax_rule_zero, fee)]
try:
ia = o.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
for tax_rule, price in fee_values:
tax_rule = tax_rule or tax_rule_zero
tax = tax_rule.tax(
price, invoice_address=ia, base_price_is="gross"
)
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=price,
value=fee,
order=o,
tax_rate=tax.rate,
tax_code=tax.code,
tax_value=tax.tax,
tax_rule=tax_rule,
tax_rule=o.event.settings.tax_rate_default,
)
f._calculate_tax()
ocm.add_fee(f)
ocm.commit()

View File

@@ -1534,10 +1534,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
total_remaining -= to_pay
if payment_fee:
if event.settings.tax_rule_payment == "default":
payment_fee_tax_rule = event.cached_default_tax_rule or TaxRule.zero()
else:
payment_fee_tax_rule = TaxRule.zero()
payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero()
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross', invoice_address=invoice_address)
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,

View File

@@ -62,7 +62,6 @@ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.media import MEDIA_TYPES
@@ -97,7 +96,6 @@ from pretix.base.services.pricing import (
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.services.tax import split_fee_for_taxes
from pretix.base.signals import (
order_approved, order_canceled, order_changed, order_denied, order_expired,
order_expiry_changed, order_fee_calculation, order_paid, order_placed,
@@ -488,7 +486,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None, keep_fees=None, cancel_invoice=True, comment=None, tax_mode=None):
cancellation_fee=None, keep_fees=None, cancel_invoice=True, comment=None):
"""
Mark this order as canceled
:param order: The order to change
@@ -508,10 +506,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
if isinstance(cancellation_fee, str):
cancellation_fee = Decimal(cancellation_fee)
elif isinstance(cancellation_fee, (float, int)):
cancellation_fee = round_decimal(cancellation_fee, order.event.currency)
tax_mode = tax_mode or order.event.settings.tax_rule_cancellation
if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.'))
@@ -539,9 +533,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
m.save()
if cancellation_fee:
positions = []
for position in order.positions.all():
positions.append(position)
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
@@ -554,38 +546,17 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if keep_fees and fee in keep_fees:
new_fee -= fee.value
else:
positions.append(fee)
fee.canceled = True
fee.save(update_fields=['canceled'])
if new_fee:
tax_rule_zero = TaxRule.zero()
if tax_mode == "default":
fee_values = [(order.event.cached_default_tax_rule or tax_rule_zero, new_fee)]
elif tax_mode == "split":
fee_values = split_fee_for_taxes(positions, new_fee, order.event)
else:
fee_values = [(tax_rule_zero, new_fee)]
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
for tax_rule, price in fee_values:
tax_rule = tax_rule or tax_rule_zero
tax = tax_rule.tax(
price, invoice_address=ia, base_price_is="gross"
)
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=price,
value=new_fee,
tax_rule=order.event.settings.tax_rate_default,
order=order,
tax_rate=tax.rate,
tax_code=tax.code,
tax_value=tax.tax,
tax_rule=tax_rule,
)
f._calculate_tax()
f.save()
if cancellation_fee > order.total:

View File

@@ -22,8 +22,6 @@
import logging
import os
import re
from collections import defaultdict
from decimal import Decimal
from xml.etree import ElementTree
import requests
@@ -34,9 +32,7 @@ from zeep import Client, Transport
from zeep.cache import SqliteCache
from zeep.exceptions import Fault
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, Event, OrderFee
from pretix.base.models.tax import TaxRule, cc_to_vat_prefix, is_eu_country
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
logger = logging.getLogger(__name__)
error_messages = {
@@ -233,64 +229,3 @@ def validate_vat_id(vat_id, country_code):
return _validate_vat_id_NO(vat_id, country_code)
raise VATIDTemporaryError(f'VAT ID should not be entered for country {country_code}')
def split_fee_for_taxes(positions: list, fee_value: Decimal, event: Event):
"""
Given a list of either OrderPosition, OrderFee or CartPosition objects and the total value
of a fee, this will return a list of [(tax_rule, fee_value)] tuples that distributes the
taxes over the same tax rules as the positions with a value representative to the value
the tax rule.
Since the input fee_value is a gross value, we also split it by the gross percentages of
positions. This will lead to the same result as if we split the net value by net percentages,
but is easier to compute.
"""
d = defaultdict(lambda: Decimal("0.00"))
tax_rule_zero = TaxRule.zero()
trs = {}
for p in positions:
if isinstance(p, CartPosition):
tr = p.item.tax_rule
v = p.price
elif isinstance(p, OrderFee):
tr = p.tax_rule
v = p.value
else:
tr = p.tax_rule
v = p.price
if not tr:
tr = tax_rule_zero
# use tr.pk as key as tax_rule_zero is not hashable
d[tr.pk] += v
trs[tr.pk] = tr
base_values = sorted([(trs[key], value) for key, value in d.items()], key=lambda t: t[0].rate)
sum_base = sum(value for key, value in base_values)
if sum_base:
fee_values = [
(key, round_decimal(fee_value * value / sum_base, event.currency))
for key, value in base_values
]
sum_fee = sum(value for key, value in fee_values)
# If there are rounding differences, we fix them up, but always leaning to the benefit of the tax
# authorities
if sum_fee > fee_value:
fee_values[0] = (
fee_values[0][0],
fee_values[0][1] + (fee_value - sum_fee),
)
elif sum_fee < fee_value:
fee_values[-1] = (
fee_values[-1][0],
fee_values[-1][1] + (fee_value - sum_fee),
)
elif len(d) == 1:
# Rare edge case: All positions are 0-valued, but have a common tax rate. Could happen e.g. with a discount
# that reduces all positions to 0, but not the shipping fees. Let's use that tax rate!
fee_values = [(list(trs.values())[0], fee_value)]
else:
# All positions are zero-valued, and we have no clear tax rate, so we use the default tax rate.
fee_values = [(event.cached_default_tax_rule or tax_rule_zero, fee_value)]
return fee_values

View File

@@ -66,7 +66,7 @@ from pretix.api.serializers.fields import (
)
from pretix.api.serializers.i18n import I18nURLField
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
from pretix.base.models.tax import VAT_ID_COUNTRIES
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
SerializerRelativeDateField, SerializerRelativeDateTimeField,
@@ -1027,47 +1027,9 @@ DEFAULTS = {
widget=forms.CheckboxInput,
)
},
'tax_rule_payment': {
'default': 'default',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('default', _('Use default tax rate')),
('none', _('Charge no taxes')),
),
),
'form_kwargs': dict(
label=_("Tax handling on payment fees"),
widget=forms.RadioSelect,
choices=(
('default', _('Use default tax rate')),
('none', _('Charge no taxes')),
),
)
},
'tax_rule_cancellation': {
'default': 'none',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('none', _('Charge no taxes')),
('split', _('Use same taxes as order positions (split according to net prices)')),
('default', _('Use default tax rate')),
),
),
'form_kwargs': dict(
label=_("Tax handling on cancellation fees"),
widget=forms.RadioSelect,
choices=(
('none', _('Charge no taxes')),
('split', _('Use same taxes as order positions (split according to net prices)')),
('default', _('Use default tax rate')),
),
)
'tax_rate_default': {
'default': None,
'type': TaxRule
},
'invoice_generate': {
'default': 'False',

View File

@@ -1,52 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io 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 django.http import HttpResponse
from django.templatetags.static import static
from django.views.decorators.cache import cache_page
@cache_page(3600)
def webmanifest(request):
return HttpResponse(
"""{
"name": "",
"short_name": "",
"icons": [
{
"src": "%s",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "%s",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#3b1c4a",
"background_color": "#3b1c4a",
"display": "standalone"
}""" % (
static('pretixbase/img/icons/android-chrome-192x192.png'),
static('pretixbase/img/icons/android-chrome-512x512.png'),
), content_type='text/json'
)

View File

@@ -761,7 +761,6 @@ class CancelSettingsForm(SettingsForm):
'change_allow_user_addons',
'change_allow_user_if_checked_in',
'change_allow_attendee',
'tax_rule_cancellation',
]
def __init__(self, *args, **kwargs):
@@ -784,8 +783,14 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
'payment_term_accept_late',
'payment_pending_hidden',
'payment_explanation',
'tax_rule_payment',
]
tax_rate_default = forms.ModelChoiceField(
queryset=TaxRule.objects.none(),
label=_('Tax rule for payment fees'),
required=False,
help_text=_("The tax rule that applies for additional fees you configured for single payment methods. This "
"will set the tax rate and reverse charge rules, other settings of the tax rule are ignored.")
)
def clean_payment_term_days(self):
value = self.cleaned_data.get('payment_term_days')
@@ -799,6 +804,10 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
raise ValidationError(_("This field is required."))
return value
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tax_rate_default'].queryset = self.obj.tax_rules.all()
class ProviderForm(SettingsForm):
"""

View File

@@ -406,6 +406,7 @@ class ItemCreateForm(I18nModelForm):
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"),
queryset=self.event.items.all(),
@@ -415,8 +416,6 @@ class ItemCreateForm(I18nModelForm):
)
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
else:
self.fields['tax_rule'].empty_label = _('No taxation')
if not self.event.has_subevents:
choices = [

View File

@@ -174,7 +174,8 @@ class CancelForm(forms.Form):
label=_('Keep a cancellation fee of'),
help_text=_('If you keep a fee, all positions within this order will be canceled and the order will be reduced '
'to a cancellation fee. Payment and shipping fees will be canceled as well, so include them '
'in your cancellation fee if you want to keep them.'),
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'),
)
cancel_invoice = forms.BooleanField(
label=_('Generate cancellation for invoice'),
@@ -199,19 +200,6 @@ class CancelForm(forms.Form):
self.fields['cancellation_fee'].max_value = self.instance.total
if not self.instance.invoices.exists():
del self.fields['cancel_invoice']
if self.instance.event.settings.tax_rule_cancellation == 'split':
self.fields['cancellation_fee'].help_text = str(self.fields['cancellation_fee'].help_text) + ' ' + _(
'Please enter a gross amount. As per your event settings, the taxes will be split the same way as the '
'order positions.'
)
elif self.instance.event.settings.tax_rule_cancellation == 'default':
self.fields['cancellation_fee'].help_text = str(self.fields['cancellation_fee'].help_text) + ' ' + _(
'Please enter a gross amount. As per your event settings, the default tax rate will be charged.'
)
elif self.instance.event.settings.tax_rule_cancellation == 'none':
self.fields['cancellation_fee'].help_text = str(self.fields['cancellation_fee'].help_text) + ' ' + _(
'As per your event settings, no tax will be charged.'
)
def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')

View File

@@ -20,8 +20,10 @@
{% endif %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}">
<link rel="icon" type="image/png" sizes="192x192" href="{% static "pretixbase/img/icons/android-chrome-192x192.png" %}">
<link rel="manifest" href="{% url "site.webmanifest" %}">
<link rel="manifest" href="{% url "presale:site.webmanifest" %}">
<link rel="mask-icon" href="{% static "pretixbase/img/icons/safari-pinned-tab.svg" %}" color="#3b1c4a">
<meta name="msapplication-TileColor" content="#3b1c4a">
<meta name="msapplication-config" content="{% url "presale:browserconfig.xml" %}">
<meta name="theme-color" content="#3b1c4a">
</head>
<body>

View File

@@ -79,8 +79,10 @@
{% endif %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}">
<link rel="icon" type="image/png" sizes="192x192" href="{% static "pretixbase/img/icons/android-chrome-192x192.png" %}">
<link rel="manifest" href="{% url "site.webmanifest" %}">
<link rel="manifest" href="{% url "presale:site.webmanifest" %}">
<link rel="mask-icon" href="{% static "pretixbase/img/icons/safari-pinned-tab.svg" %}" color="#3b1c4a">
<meta name="msapplication-TileColor" content="#3b1c4a">
<meta name="msapplication-config" content="{% url "presale:browserconfig.xml" %}">
<meta name="theme-color" content="#3b1c4a">
<meta name="referrer" content="origin">

View File

@@ -26,7 +26,6 @@
{% bootstrap_field form.cancel_allow_user_paid_keep layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}
{% bootstrap_field form.tax_rule_cancellation layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_until layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees layout="control" %}
<div data-display-dependency="#id_cancel_allow_user_paid_adjust_fees">

View File

@@ -79,7 +79,7 @@
<fieldset>
<legend>{% trans "Advanced" %}</legend>
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.tax_rule_payment layout="control" %}
{% bootstrap_field form.tax_rate_default layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %}
</fieldset>
</div>

View File

@@ -9,10 +9,11 @@
{% if possible %}
<p>{% blocktrans %}Are you sure you want to delete the tax rule <strong>{{ taxrule }}</strong>?{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans %}You cannot delete a tax rule that is in use for a product, has been in use for any existing orders, or is the default tax rule of the event.{% endblocktrans %}</p>
<p>{% blocktrans %}You cannot delete a tax rule that is in use for a product or has been in use for any existing orders.{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.settings.tax" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
<a href="{% url "control:event.settings.tax" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn
btn-default btn-cancel">
{% trans "Cancel" %}
</a>
{% if possible %}

View File

@@ -24,7 +24,6 @@
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Default" %}</th>
<th>{% trans "Rate" %}</th>
<th class="action-col-2"></th>
</tr>
@@ -37,22 +36,6 @@
{{ tr.internal_name|default:tr.name }}
</a></strong>
</td>
<td>
{% if tr.default %}
<span class="text-success">
<span class="fa fa-check"></span>
{% trans "Default" %}
</span>
{% else %}
<form class="form-inline" method="post"
action="{% url "control:event.settings.tax.default" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
{% csrf_token %}
<button class="btn btn-default btn-sm">
{% trans "Make default" %}
</button>
</form>
{% endif %}
</td>
<td>
{% if tr.price_includes_tax %}
{% blocktrans with rate=tr.rate%}incl. {{ rate }} %{% endblocktrans %}

View File

@@ -1,6 +1,5 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load icon %}
{% load static %}
{% load compress %}
{% block title %}{% trans "PDF Editor" %}{% endblock %}
@@ -26,103 +25,28 @@
<div class="col-md-9">
<div class="panel panel-default panel-pdf-editor">
<div class="panel-heading">
<form method="post" action="" id="preview-form" target="_blank" class="pull-right flip">
{% csrf_token %}
<input type="hidden" value="" name="data">
<input type="hidden" value="" name="background">
<input type="hidden" value="true" name="preview">
<div class="pull-right flip">
<div class="btn-group">
<button type="button" class="btn btn-default" id="toolbox-source"
<button type="button" class="btn btn-default btn-xs" id="toolbox-source"
title="{% trans "Code" %}">
{% icon "code" %}
<span class="fa fa-code"></span>
</button>
<button type="submit" class="btn btn-default" id="editor-preview">
{% icon "eye" %}
{% trans "Preview" %}
<button type="button" class="btn btn-default btn-xs" id="toolbox-paste"
title="{% trans "Paste" %}">
<span class="fa fa-paste"></span>
</button>
</div>
<button type="submit" class="btn btn-primary btn-save" id="editor-save">
{% icon "save" %}
{% trans "Save" %}
</button>
</form>
<div class="btn-group add-buttons">
<button class="btn btn-default" id="editor-add-textcontainer" disabled>
{% icon "font" %}
{% trans "Text box" %}
</button>
<div class="btn-group dropdown">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
{% icon "qrcode" %}
{% trans "QR Code" %}
</button>
<ul class="dropdown-menu">
<li>
<button class="btn" id="editor-add-qrcode" data-content="secret" disabled>
{% trans "QR code for Check-In" %}
</button>
</li>
<li>
<button class="btn" id="editor-add-qrcode-lead"
data-content="pseudonymization_id"
disabled>
{% trans "QR code for Lead Scanning" %}
</button>
</li>
<li>
<button class="btn" id="editor-add-qrcode-other"
data-content="other"
disabled>
{% trans "Other QR code" %}
</button>
</li>
</div>
<div class="btn-group dropdown">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
{% icon "image" %}
{% trans "Image" %}
</button>
<ul class="dropdown-menu">
<li>
<button class="btn" id="editor-add-image" disabled
data-toggle="tooltip" title="{% trans "You can use this to add user-uploaded pictures from questions or pictures generated by plugins. If you want to embed a logo or other images, use a custom background instead." %}">
{% trans "Dynamic image" %}
</button>
</li>
<li>
<button class="btn" id="editor-add-poweredby"
data-content="dark"
disabled>
{% trans "pretix Logo" %}
</button>
</li>
</div>
</div>
<div class="btn-group object-buttons">
<button type="button" class="btn btn-default" id="toolbox-duplicate"
title="{% trans "Duplicate" %}">
{% icon "copy" %}
</button>
<button type="button" class="btn btn-default" id="toolbox-delete"
title="{% trans "Delete" %}">
{% icon "trash" class="text-danger" %}
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default" id="toolbox-undo"
<button type="button" class="btn btn-default btn-xs" id="toolbox-undo"
title="{% trans "Undo" %}">
{% icon "undo" %}
<span class="fa fa-undo"></span>
</button>
<button type="button" class="btn btn-default" id="toolbox-redo"
<button type="button" class="btn btn-default btn-xs" id="toolbox-redo"
title="{% trans "Redo" %}">
{% icon "repeat" %}
<span class="fa fa-repeat"></span>
</button>
</div>
</div>
{% trans "Editor" %}
</div>
<div class="panel-body">
<ul class="nav nav-pills" id="page_nav">
</ul>
@@ -229,6 +153,22 @@
<div class="col-md-3" id="editor-toolbox-area">
<div class="panel panel-default" id="toolbox">
<div class="panel-heading">
<div class="pull-right object-buttons flip">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs" id="toolbox-cut"
title="{% trans "Cut" %}">
<span class="fa fa-cut"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-copy"
title="{% trans "Copy" %}">
<span class="fa fa-copy"></span>
</button>
<button type="button" class="btn btn-danger btn-xs" id="toolbox-delete"
title="{% trans "Delete" %}">
<span class="fa fa-trash"></span>
</button>
</div>
</div>
<span id="toolbox-heading">
{% trans "Loading…" %}
</span>
@@ -548,6 +488,63 @@
</div>
</div>
</div>
<div class="editor-toolbox-text panel panel-default">
<div class="panel-heading">
{% trans "Add a new object" %}
</div>
<div class="panel-body add-buttons">
<button class="btn btn-default btn-block" id="editor-add-textcontainer" disabled>
<span class="fa fa-font"></span>
{% trans "Text box" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-text" disabled>
<span class="fa fa-font"></span>
{% trans "Text (deprecated)" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-qrcode" data-content="secret" disabled>
<span class="fa fa-qrcode"></span>
{% trans "QR code for Check-In" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-qrcode-lead"
data-content="pseudonymization_id"
disabled>
<span class="fa fa-qrcode"></span>
{% trans "QR code for Lead Scanning" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-qrcode-other"
data-content="secret"
disabled>
<span class="fa fa-qrcode"></span>
{% trans "Other QR code" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-poweredby"
data-content="dark"
disabled>
<span class="fa fa-image"></span>
{% trans "pretix Logo" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-image" disabled
data-toggle="tooltip" title="{% trans "You can use this to add user-uploaded pictures from questions or pictures generated by plugins. If you want to embed a logo or other images, use a custom background instead." %}">
<span class="fa fa-image"></span>
{% trans "Dynamic image" %}
</button>
</div>
</div>
<form method="post" action="" id="preview-form" target="_blank">
<div class="form-group submit-group">
{% csrf_token %}
<input type="hidden" value="" name="data">
<input type="hidden" value="" name="background">
<input type="hidden" value="true" name="preview">
<button type="submit" class="btn btn-default btn-lg" id="editor-preview">
{% trans "Preview" %}
</button>
<button type="submit" class="btn btn-primary btn-save" id="editor-save">
<span class="fa fa-fw fa-save"></span>
{% trans "Save" %}
</button>
</div>
</form>
<p>&nbsp;</p>
<div class="alert alert-info" id="version-notice">
{% blocktrans trimmed with print_version="2.18" scan_version="1.22" %}

View File

@@ -288,7 +288,6 @@ urlpatterns = [
re_path(r'^settings/tax/(?P<rule>\d+)/$', event.TaxUpdate.as_view(), name='event.settings.tax.edit'),
re_path(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'),
re_path(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'),
re_path(r'^settings/tax/(?P<rule>\d+)/default$', event.TaxDefault.as_view(), name='event.settings.tax.default'),
re_path(r'^settings/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'),
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),

View File

@@ -68,7 +68,7 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
from django.views.generic import DetailView, FormView, ListView
from django.views.generic import FormView, ListView
from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString
@@ -1274,8 +1274,6 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView
@transaction.atomic
def form_valid(self, form):
if not self.request.event.tax_rules.exists():
form.instance.default = True
form.instance.event = self.request.event
form.instance.custom_rules = json.dumps([
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
@@ -1356,50 +1354,6 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
return super().form_invalid(form)
class TaxDefault(EventSettingsViewMixin, EventPermissionRequiredMixin, DetailView):
model = TaxRule
permission = 'can_change_event_settings'
def get_object(self, queryset=None) -> TaxRule:
try:
return self.request.event.tax_rules.get(
id=self.kwargs['rule']
)
except TaxRule.DoesNotExist:
raise Http404(_("The requested tax rule does not exist."))
def get(self, request, *args, **kwargs):
return self.http_method_not_allowed(request, *args, **kwargs)
@transaction.atomic
def post(self, request, *args, **kwargs):
messages.success(self.request, _('Your changes have been saved.'))
obj = self.get_object()
if not obj.default:
for tr in self.request.event.tax_rules.filter(default=True):
tr.log_action(
'pretix.event.taxrule.changed', user=self.request.user, data={
'default': False,
}
)
tr.default = False
tr.save(update_fields=['default'])
obj.log_action(
'pretix.event.taxrule.changed', user=self.request.user, data={
'default': True,
}
)
obj.default = True
obj.save(update_fields=['default'])
return redirect(self.get_success_url())
def get_success_url(self) -> str:
return reverse('control:event.settings.tax', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, CompatDeleteView):
model = TaxRule
template_name = 'pretixcontrol/event/tax_delete.html'

View File

@@ -181,9 +181,8 @@ class EventWizard(SafeSessionWizardView):
initial['location'] = self.clone_from.location
initial['timezone'] = self.clone_from.settings.timezone
initial['locale'] = self.clone_from.settings.locale
tax_rule = self.clone_from.cached_default_tax_rule
if tax_rule:
initial['tax_rate'] = tax_rule.rate
if self.clone_from.settings.tax_rate_default:
initial['tax_rate'] = self.clone_from.settings.tax_rate_default.rate
if 'organizer' in self.request.GET:
if step == 'foundation':
try:
@@ -326,17 +325,10 @@ class EventWizard(SafeSessionWizardView):
event.set_defaults()
if basics_data['tax_rate'] is not None:
if self.clone_from:
default_tax_rule = self.clone_from.cached_default_tax_rule
elif copy_data and copy_data['copy_from_event']:
default_tax_rule = from_event.cached_default_tax_rule
else:
default_tax_rule = None
if not default_tax_rule or default_tax_rule.rate != basics_data['tax_rate']:
event.tax_rules.create(
if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']:
event.settings.tax_rate_default = event.tax_rules.create(
name=LazyI18nString.from_gettext(gettext('VAT')),
rate=basics_data['tax_rate'],
default=not default_tax_rule,
rate=basics_data['tax_rate']
)
event.settings.set('timezone', basics_data['timezone'])

View File

@@ -22,10 +22,11 @@
import logging
from django import template
from django.core.cache import cache
from django.core.files.storage import default_storage
from pretix import settings
from pretix.helpers.thumb import get_thumbnail
from pretix.helpers.thumb import get_srcset_sizes, get_thumbnail
register = template.Library()
logger = logging.getLogger(__name__)
@@ -46,3 +47,30 @@ def thumb(source, arg):
# default_storage.url works for all files in NanoCDNStorage. For others, this may return an invalid URL.
# But for a fallback, this can probably be accepted.
return source.url if hasattr(source, 'url') else default_storage.url(str(source))
@register.filter
def thumbset(source, arg):
cache_key = f"thumbset:{source}:{arg}"
cached_thumbset = cache.get(cache_key)
if cached_thumbset is not None:
return cached_thumbset
formats = list(set().union(
settings.PILLOW_FORMATS_IMAGE,
settings.PILLOW_FORMATS_QUESTIONS_FAVICON,
settings.PILLOW_FORMATS_QUESTIONS_IMAGE
))
srcs = []
if not cached_thumbset:
for thumbsize, factor in get_srcset_sizes(arg):
try:
t = get_thumbnail(source, thumbsize, formats=formats, skip_if_limited_by_input=True)
if t:
srcs.append(f"{t.thumb.url} {factor}")
except:
logger.exception(f'Failed to create thumbnail of {source} at {thumbsize}')
srcset = ", ".join(srcs)
cache.set(cache_key, srcset, timeout=3600)
return srcset

View File

@@ -96,7 +96,27 @@ def get_minsize(size):
return (min_width, min_height)
def get_srcset_sizes(size):
w, h = size.split("x")
for m in (2, 3):
if w.endswith("_"):
new_w = f"{int(w.rstrip('_')) * m}_"
else:
new_w = f"{int(w) * m}"
if h.endswith("_"):
new_h = f"{int(h.rstrip('_')) * m}_"
elif h.endswith("^"):
new_h = f"{int(h.rstrip('^')) * m}^"
else:
new_h = f"{int(h) * m}"
yield f"{new_w}x{new_h}", f"{m}x"
def get_sizes(size, imgsize):
"""
:return: Tuple of (new_size, crop_box, size_limited_by_input)
"""
crop = False
if size.endswith('^'):
crop = True
@@ -112,37 +132,48 @@ def get_sizes(size, imgsize):
else:
size = [int(size), int(size)]
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
limited_by_input = size[1] > imgsize[1] and size[0] > imgsize[0]
if crop:
# currently crop and min-size cannot be combined
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
if wfactor == hfactor:
return (int(imgsize[0] * wfactor), int(imgsize[1] * hfactor)), \
return (
(int(imgsize[0] * wfactor), int(imgsize[1] * hfactor)),
(0, int((imgsize[1] * wfactor - imgsize[1] * hfactor) / 2),
imgsize[0] * hfactor, int((imgsize[1] * wfactor + imgsize[1] * wfactor) / 2))
imgsize[0] * hfactor, int((imgsize[1] * wfactor + imgsize[1] * wfactor) / 2)),
limited_by_input
)
elif wfactor > hfactor:
return (int(size[0]), int(imgsize[1] * wfactor)), \
(0, int((imgsize[1] * wfactor - size[1]) / 2), size[0], int((imgsize[1] * wfactor + size[1]) / 2))
return (
(int(size[0]), int(imgsize[1] * wfactor)),
(0, int((imgsize[1] * wfactor - size[1]) / 2), size[0], int((imgsize[1] * wfactor + size[1]) / 2)),
limited_by_input
)
else:
return (int(imgsize[0] * hfactor), int(size[1])), \
(int((imgsize[0] * hfactor - size[0]) / 2), 0, int((imgsize[0] * hfactor + size[0]) / 2), size[1])
return (
(int(imgsize[0] * hfactor), int(size[1])),
(int((imgsize[0] * hfactor - size[0]) / 2), 0, int((imgsize[0] * hfactor + size[0]) / 2), size[1]),
limited_by_input
)
else:
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
if wfactor == hfactor:
return (int(imgsize[0] * hfactor), int(imgsize[1] * wfactor)), None
return (int(imgsize[0] * hfactor), int(imgsize[1] * wfactor)), None, limited_by_input
elif wfactor < hfactor:
return (size[0], int(imgsize[1] * wfactor)), None
return (size[0], int(imgsize[1] * wfactor)), None, limited_by_input
else:
return (int(imgsize[0] * hfactor), size[1]), None
return (int(imgsize[0] * hfactor), size[1]), None, limited_by_input
def resize_image(image, size):
"""
:return: Tuple of (new_image, size_limited_by_input)
"""
# before we calc thumbnail, we need to check and apply EXIF-orientation
image = ImageOps.exif_transpose(image)
new_size, crop = get_sizes(size, image.size)
new_size, crop, limited_by_input = get_sizes(size, image.size)
image = image.resize(new_size, resample=Resampling.LANCZOS)
if crop:
image = image.crop(crop)
@@ -162,10 +193,10 @@ def resize_image(image, size):
image = image.crop((new_x, new_y, new_x + new_width, new_y + new_height))
return image
return image, limited_by_input
def create_thumbnail(source, size, formats=None):
def create_thumbnail(source, size, formats=None, skip_if_limited_by_input=False):
source_name = str(source)
# HACK: this ensures that the file is opened in binary mode, which is not guaranteed otherwise, esp. for
@@ -181,10 +212,17 @@ def create_thumbnail(source, size, formats=None):
raise ThumbnailError('Could not load image')
frames = []
any_limited_by_input = False
durations = []
for f in ImageSequence.Iterator(image):
durations.append(f.info.get("duration", 1000))
frames.append(resize_image(f, size))
img, limited_by_input = resize_image(f, size)
any_limited_by_input = any_limited_by_input or limited_by_input
frames.append(img)
if any_limited_by_input and skip_if_limited_by_input:
return
image_out = frames[0]
save_kwargs = {}
source_ext = os.path.splitext(source_name)[1].lower()
@@ -223,10 +261,10 @@ def create_thumbnail(source, size, formats=None):
return t
def get_thumbnail(source, size, formats=None):
def get_thumbnail(source, size, formats=None, skip_if_limited_by_input=False):
# Assumes files are immutable
try:
source_name = str(source)
return Thumbnail.objects.get(source=source_name, size=size)
except Thumbnail.DoesNotExist:
return create_thumbnail(source, size, formats=formats)
return create_thumbnail(source, size, formats=formats, skip_if_limited_by_input=skip_if_limited_by_input)

View File

@@ -37,6 +37,9 @@
<link rel="icon" type="image/png" sizes="192x192" href="{% static "pretixbase/img/icons/android-chrome-192x192.png" %}">
<link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}">
{% endif %}
<link rel="manifest" href="{% url "presale:site.webmanifest" %}">
<meta name="msapplication-TileColor" content="{{ settings.primary_color|default:"#3b1c4a" }}">
<meta name="msapplication-config" content="{% url "presale:browserconfig.xml" %}">
<meta name="theme-color" content="{{ settings.primary_color|default:"#3b1c4a" }}">
</head>
<body class="nojs" data-locale="{{ request.LANGUAGE_CODE }}" data-now="{% now "U.u" %}" data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-currency="{{ request.event.currency }}">

View File

@@ -85,12 +85,14 @@
{% if event_logo and event_logo_image_large %}
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}"
title="{% trans 'Homepage' %}">
<img src="{{ event_logo|thumb:'1170x5000' }}" alt="{{ event.name }}" class="event-logo" />
<img src="{{ event_logo|thumb:'1170x5000' }}" srcset="{{ event_logo|thumbset:'1170x5000' }}"
alt="{{ event.name }}" class="event-logo" />
</a>
{% elif event_logo %}
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}"
title="{% trans 'Homepage' %}">
<img src="{{ event_logo|thumb:'5000x120' }}" alt="{{ event.name }}" class="event-logo" />
<img src="{{ event_logo|thumb:'5000x120' }}" srcset="{{ event_logo|thumbset:'5000x120' }}"
alt="{{ event.name }}" class="event-logo" />
</a>
{% else %}
<h1>

View File

@@ -48,6 +48,7 @@
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumb:'60x60^' }}"
srcset="{{ item.picture|thumbset:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
@@ -239,6 +240,7 @@
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumb:'60x60^' }}"
srcset="{{ item.picture|thumbset:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}

View File

@@ -39,6 +39,7 @@
data-lightbox="{{ item.id }}"
aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}">
<img src="{{ item.picture|thumb:'60x60^' }}"
srcset="{{ item.picture|thumbset:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
@@ -258,6 +259,7 @@
data-lightbox="{{ item.id }}"
aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}">
<img src="{{ item.picture|thumb:'60x60^' }}"
srcset="{{ item.picture|thumbset:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}

View File

@@ -93,6 +93,7 @@
data-lightbox="{{ item.id }}"
aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}">
<img src="{{ item.picture|thumb:'60x60^' }}"
srcset="{{ item.picture|thumbset:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
@@ -274,6 +275,7 @@
data-lightbox="{{ item.id }}"
aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}">
<img src="{{ item.picture|thumb:'60x60^' }}"
srcset="{{ item.picture|thumbset:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}

View File

@@ -53,12 +53,14 @@
{% endif %}
{% if organizer_logo and organizer.settings.organizer_logo_image_large %}
<a href="{% eventurl organizer "presale:organizer.index" %}" title="{{ organizer.name }}">
<img src="{{ organizer_logo|thumb:'1170x5000' }}" alt="{{ organizer.name }}"
<img src="{{ organizer_logo|thumb:'1170x5000' }}" srcset="{{ organizer_logo|thumbset:'1170x5000' }}"
alt="{{ organizer.name }}"
class="organizer-logo" />
</a>
{% elif organizer_logo %}
<a href="{% eventurl organizer "presale:organizer.index" %}" title="{{ organizer.name }}">
<img src="{{ organizer_logo|thumb:'5000x120' }}" alt="{{ organizer.name }}"
<img src="{{ organizer_logo|thumb:'5000x120' }}" srcset="{{ organizer_logo|thumbset:'5000x120' }}"
alt="{{ organizer.name }}"
class="organizer-logo" />
</a>
{% else %}

View File

@@ -235,5 +235,7 @@ organizer_patterns = [
locale_patterns = [
re_path(r'^locale/set$', pretix.presale.views.locale.LocaleSet.as_view(), name='locale.set'),
re_path(r'^robots.txt$', pretix.presale.views.robots.robots_txt, name='robots.txt'),
re_path(r'^browserconfig.xml$', pretix.presale.views.theme.browserconfig_xml, name='browserconfig.xml'),
re_path(r'^site.webmanifest$', pretix.presale.views.theme.webmanifest, name='site.webmanifest'),
path('widget/v<int:version>.<slug:lang>.js', pretix.presale.views.widget.widget_js, name='widget.js'),
]

View File

@@ -24,7 +24,9 @@ import time
from django.contrib.staticfiles import finders
from django.http import HttpResponse
from django.templatetags.static import static
from django.utils.http import http_date
from django.views.decorators.cache import cache_page
from django.views.decorators.gzip import gzip_page
from django.views.decorators.http import condition
@@ -42,6 +44,53 @@ def _get_source_cache_key():
return _source_cache_key
@cache_page(3600)
def browserconfig_xml(request):
return HttpResponse(
"""<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="{}"/>
<square310x310logo src="{}"/>
<TileColor>#3b1c4a</TileColor>
</tile>
</msapplication>
</browserconfig>""".format(
static('pretixbase/img/icons/mstile-150x150.png'),
static('pretixbase/img/icons/mstile-310x310.png'),
), content_type='text/xml'
)
@cache_page(3600)
def webmanifest(request):
return HttpResponse(
"""{
"name": "",
"short_name": "",
"icons": [
{
"src": "%s",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "%s",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#3b1c4a",
"background_color": "#3b1c4a",
"display": "standalone"
}""" % (
static('pretixbase/img/icons/android-chrome-192x192.png'),
static('pretixbase/img/icons/android-chrome-512x512.png'),
), content_type='text/json'
)
@gzip_page
@condition(etag_func=lambda request, **kwargs: request.GET.get("version"))
def theme_css(request, **kwargs):

View File

@@ -218,7 +218,7 @@ def widget_js(request, version, lang, **kwargs):
return resp
gs = GlobalSettingsObject()
fname = gs.settings.get('widget_file_v{}_{}'.format(version, lang))
fname = gs.settings.get('widget_file_{}_{}'.format(version, lang))
resp = None
if fname and not settings.DEBUG:
if isinstance(fname, File):
@@ -238,8 +238,8 @@ def widget_js(request, version, lang, **kwargs):
'widget/widget.{}.{}.{}.js'.format(version, lang, checksum),
ContentFile(data)
)
gs.settings.set('widget_file_v{}_{}'.format(version, lang), 'file://' + newname)
gs.settings.set('widget_checksum_v{}_{}'.format(version, lang), checksum)
gs.settings.set('widget_file_{}_{}'.format(version, lang), 'file://' + newname)
gs.settings.set('widget_checksum_{}_{}'.format(version, lang), checksum)
cache.set('widget_js_data_v{}_{}'.format(version, lang), data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp._csp_ignore = True

View File

@@ -898,7 +898,6 @@ var editor = {
_update_toolbox: function () {
var selected = editor.fabric.getActiveObjects();
$(".object-buttons button").prop("disabled", selected.length == 0);
if (selected.length > 1) {
$("#toolbox").attr("data-type", "group");
$("#toolbox-heading").text(gettext("Group of objects"));
@@ -1049,11 +1048,8 @@ var editor = {
},
_cut: function () {
var thing = editor.fabric.getActiveObject();
if (!thing) {
return false;
}
editor._history_modification_in_progress = true;
var thing = editor.fabric.getActiveObject();
if (thing.type === "activeSelection") {
editor.clipboard = editor.dump(thing._objects);
thing.forEachObject(function (o) {
@@ -1070,16 +1066,15 @@ var editor = {
},
_copy: function () {
editor._history_modification_in_progress = true;
var thing = editor.fabric.getActiveObject();
if (!thing) {
return false;
}
if (thing.type === "activeSelection") {
editor.clipboard = editor.dump(thing._objects);
} else {
editor.clipboard = editor.dump([thing]);
}
return true;
editor._history_modification_in_progress = false;
editor._create_savepoint();
},
_paste: function () {
@@ -1103,19 +1098,8 @@ var editor = {
editor._create_savepoint();
},
_duplicate: function () {
var prevClipboad = editor.clipboard;
if (editor._copy()) {
editor._paste();
editor.clipboard = prevClipboad;
}
},
_delete: function () {
var thing = editor.fabric.getActiveObject();
if (!thing) {
return false;
}
if (thing.type === "activeSelection") {
thing.forEachObject(function (o) {
editor.fabric.remove(o);
@@ -1135,79 +1119,64 @@ var editor = {
if ($("#source-container").is(':visible')) {
return true;
}
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case "a":
editor._selectAll();
break;
case "y":
editor._redo();
break;
case "z":
editor._undo();
break;
case "x":
editor._cut();
break;
case "v":
editor._paste();
break;
case "c":
editor._copy();
break;
case "d":
editor._duplicate();
break;
default:
return;
}
} else {
switch (e.key) {
case "ArrowUp":
switch (e.keyCode) {
case 38: /* Up arrow */
thing.set('top', thing.get('top') - step);
thing.setCoords();
editor._create_savepoint();
break;
case "ArrowDown":
case 40: /* Down arrow */
thing.set('top', thing.get('top') + step);
thing.setCoords();
editor._create_savepoint();
break;
case "ArrowLeft":
case 37: /* Left arrow */
thing.set('left', thing.get('left') - step);
thing.setCoords();
editor._create_savepoint();
break;
case "ArrowRight":
case 39: /* Right arrow */
thing.set('left', thing.get('left') + step);
thing.setCoords();
editor._create_savepoint();
break;
case "Backspace":
case "Del":
case "Delete":
case 8: /* Backspace */
case 46: /* Delete */
editor._delete();
break;
case "Cut":
editor._cut();
case 65: /* A */
if (e.ctrlKey || e.metaKey) {
editor._selectAll();
}
break;
case "Copy":
editor._copy();
break;
case "Paste":
editor._paste();
break;
case "Redo":
case 89: /* Y */
if (e.ctrlKey || e.metaKey) {
editor._redo();
}
break;
case "Undo":
case 90: /* Z */
if (e.ctrlKey || e.metaKey) {
editor._undo();
}
break;
case 88: /* X */
if (e.ctrlKey || e.metaKey) {
editor._cut();
}
break;
case 86: /* V */
if (e.ctrlKey || e.metaKey) {
editor._paste();
}
break;
case 67: /* C */
if (e.ctrlKey || e.metaKey) {
editor._copy();
}
break;
default:
return;
}
}
e.preventDefault();
editor.fabric.renderAll();
editor._update_toolbox_values();
@@ -1265,9 +1234,6 @@ var editor = {
} else {
$("#editor-save").addClass("btn-success").removeClass("btn-primary").find(".fa").attr("class", "fa fa-fw fa-check");
}
$("#toolbox-undo").prop("disabled", editor._history_pos == editor.history.length-1);
$("#toolbox-redo").prop("disabled", editor._history_pos == 0);
},
_save: function () {
@@ -1451,8 +1417,10 @@ var editor = {
editor._create_savepoint();
});
$("#toolbox .colorpickerfield").bind('changeColor', editor._update_values_from_toolbox);
$("#toolbox-duplicate").bind('click', editor._duplicate);
$("#toolbox-copy").bind('click', editor._copy);
$("#toolbox-cut").bind('click', editor._cut);
$("#toolbox-delete").bind('click', editor._delete);
$("#toolbox-paste").bind('click', editor._paste);
$("#toolbox-undo").bind('click', editor._undo);
$("#toolbox-redo").bind('click', editor._redo);
$("#toolbox-source").bind('click', editor._source_show);

View File

@@ -21,7 +21,8 @@ body {
#toolbox .text,
#toolbox .textarea,
#toolbox .textcontainer,
#toolbox .imagecontent {
#toolbox .imagecontent,
#toolbox .object-buttons {
display: none;
}
#toolbox[data-type] .position,
@@ -36,7 +37,8 @@ body {
#toolbox[data-type=textcontainer] .textcontainer,
#toolbox[data-type=textcontainer] .rectsize,
#toolbox[data-type=textarea] .text,
#toolbox[data-type=textarea] .textarea {
#toolbox[data-type=textarea] .textarea,
#toolbox[data-type] .object-buttons {
display: block;
}
#toolbox[data-type=text] .btn-group-justified > .btn-group.text,
@@ -90,16 +92,3 @@ body {
padding: 15px;
background: #eee;
}
.panel-pdf-editor .panel-heading {
padding: 4px;
position: sticky;
top: 0;
z-index: 1;
}
.panel-pdf-editor .panel-heading [disabled] {
box-shadow: 0px 0px 0px 1px #cccccc inset;
}
.panel-pdf-editor .panel-heading > .btn-group,
.panel-pdf-editor .panel-heading form > .btn-group {
margin-right: 1em;
}

View File

@@ -3,9 +3,6 @@
@import "../../bootstrap/scss/bootstrap/variables";
@import "../../bootstrap/scss/bootstrap/mixins";
// Equivalent of our usual #f9f9f9, but in a way that works on other background colors
$table-bg-accent: rgba(128, 128, 128, 0.05);
.pretix-widget-hidden {
display: none;
}

View File

@@ -43,7 +43,6 @@ from pretix.base.views import applepay, js_helpers
from .base.views import (
cachedfiles, csp, health, js_catalog, metrics, redirect, source,
webmanifest,
)
base_patterns = [
@@ -52,7 +51,6 @@ base_patterns = [
re_path(r'^healthcheck/$', health.healthcheck,
name='healthcheck'),
re_path(r'^redirect/$', redirect.redir_view, name='redirect'),
re_path(r'^site.webmanifest$', webmanifest.webmanifest, name='site.webmanifest'),
re_path(r'^jsi18n/(?P<lang>[a-zA-Z-_]+)/$', js_catalog.js_catalog, name='javascript-catalog'),
re_path(r'^metrics$', metrics.serve_metrics,
name='metrics'),

View File

@@ -192,7 +192,7 @@ def subevent2(event2, meta_prop):
@pytest.fixture
@scopes_disabled()
def taxrule(event):
return event.tax_rules.create(name="VAT", rate=19, code="S/standard", default=True)
return event.tax_rules.create(name="VAT", rate=19, code="S/standard")
@pytest.fixture

View File

@@ -50,7 +50,7 @@ def item2(event2):
@pytest.fixture
def taxrule(event):
return event.tax_rules.create(rate=Decimal('19.00'), code="S/standard", default=True)
return event.tax_rules.create(rate=Decimal('19.00'), code="S/standard")
@pytest.fixture
@@ -1351,7 +1351,7 @@ def test_order_mark_canceled_pending(token_client, organizer, event, order):
@pytest.mark.django_db
def test_order_mark_canceled_pending_fee_with_tax(token_client, organizer, event, order, taxrule):
djmail.outbox = []
event.settings.tax_rule_cancellation = "default"
event.settings.tax_rate_default = taxrule
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/{}/mark_canceled/'.format(
organizer.slug, event.slug, order.code

View File

@@ -31,7 +31,6 @@ TEST_TAXRULE_RES = {
'keep_gross_if_rate_changes': False,
'name': {'en': 'VAT'},
'rate': '19.00',
'default': True,
'code': 'S/standard',
'price_includes_tax': True,
'eu_reverse_charge': False,
@@ -81,45 +80,6 @@ def test_rule_create(token_client, organizer, event):
assert str(rule.home_country) == "DE"
@pytest.mark.django_db
def test_rule_create_auto_default(token_client, organizer, event):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/taxrules/'.format(organizer.slug, event.slug),
{
"name": {"en": "VAT", "de": "MwSt"},
"rate": "19.00",
"price_includes_tax": True,
"eu_reverse_charge": False,
"home_country": "DE",
},
format='json'
)
assert resp.status_code == 201
rule = TaxRule.objects.get(pk=resp.data['id'])
assert rule.default
@pytest.mark.django_db
def test_rule_create_only_one_default(token_client, taxrule, organizer, event):
assert taxrule.default
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/taxrules/'.format(organizer.slug, event.slug),
{
"name": {"en": "VAT", "de": "MwSt"},
"rate": "19.00",
"price_includes_tax": True,
"eu_reverse_charge": False,
"home_country": "DE",
"default": True,
},
format='json'
)
assert resp.status_code == 201
taxrule.refresh_from_db()
assert not taxrule.default
@pytest.mark.django_db
def test_rule_update(token_client, organizer, event, taxrule):
resp = token_client.patch(

View File

@@ -2215,7 +2215,7 @@ class EventTest(TestCase):
is_public=True,
)
event1.meta_values.create(property=prop, value="DE")
tr7 = event1.tax_rules.create(rate=Decimal('7.00'), default=True)
tr7 = event1.tax_rules.create(rate=Decimal('7.00'))
c1 = event1.categories.create(name='Tickets')
c2 = event1.categories.create(name='Workshops')
i1 = event1.items.create(name='Foo', default_price=Decimal('13.00'), tax_rule=tr7,
@@ -2228,6 +2228,7 @@ class EventTest(TestCase):
que1 = event1.questions.create(question="Age", type="N")
que1.items.add(i1)
event1.settings.foo_setting = 23
event1.settings.tax_rate_default = tr7
cl1 = event1.checkin_lists.create(
name="All", all_products=False,
rules={
@@ -2270,7 +2271,7 @@ class EventTest(TestCase):
assert que1new.type == que1.type
assert que1new.items.get(pk=i1new.pk)
assert event2.settings.foo_setting == '23'
assert event2.cached_default_tax_rule == trnew
assert event2.settings.tax_rate_default == trnew
assert event2.checkin_lists.count() == 1
clnew = event2.checkin_lists.first()
assert [i.pk for i in clnew.limit_products.all()] == [i1new.pk]

View File

@@ -1117,7 +1117,7 @@ class OrderCancelTests(TestCase):
self.order.save()
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
with pytest.raises(OrderError):
cancel_order(self.order.pk, cancellation_fee=Decimal("50.00"))
cancel_order(self.order.pk, cancellation_fee=50)
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PAID
assert self.order.total == 46
@@ -1131,7 +1131,7 @@ class OrderCancelTests(TestCase):
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
self.op1.voucher = self.event.vouchers.create(item=self.ticket, redeemed=1)
self.op1.save()
cancel_order(self.order.pk, cancellation_fee=Decimal("2.50"))
cancel_order(self.order.pk, cancellation_fee=2.5)
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PAID
self.op1.refresh_from_db()
@@ -1158,7 +1158,7 @@ class OrderCancelTests(TestCase):
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
self.op1.voucher = self.event.vouchers.create(item=self.ticket, redeemed=1)
self.op1.save()
cancel_order(self.order.pk, cancellation_fee=Decimal("2.50"))
cancel_order(self.order.pk, cancellation_fee=2.5)
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PAID
self.op1.refresh_from_db()
@@ -1172,7 +1172,7 @@ class OrderCancelTests(TestCase):
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='testdummy_partialrefund'
)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('44.00')
@@ -1190,7 +1190,7 @@ class OrderCancelTests(TestCase):
provider='giftcard',
info='{"gift_card": %d}' % gc.pk
)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('44.00')
@@ -1209,7 +1209,7 @@ class OrderCancelTests(TestCase):
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='testdummy_partialrefund'
)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert gc.value == Decimal('0.00')
@@ -1224,7 +1224,7 @@ class OrderCancelTests(TestCase):
provider='testdummy_partialrefund'
)
with pytest.raises(OrderError):
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
assert gc.value == Decimal('20.00')
@classscope(attr='o')
@@ -1234,7 +1234,7 @@ class OrderCancelTests(TestCase):
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='testdummy_fullrefund'
)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
assert not self.order.refunds.exists()
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
@@ -1258,7 +1258,7 @@ class OrderChangeManagerTests(TestCase):
provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CREATED, amount=self.order.total
)
self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00'))
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'), default=True)
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rule=self.tr7,
default_price=Decimal('23.00'), admission=True)
self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rule=self.tr7,
@@ -1868,6 +1868,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_payment_fee_calculation(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm.change_price(self.op1, Decimal('24.00'))
@@ -1881,6 +1882,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_pending_free_order_stays_pending(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
self.ocm.change_price(self.op1, Decimal('0.00'))
self.ocm.change_price(self.op2, Decimal('0.00'))
self.ocm.commit()
@@ -2268,6 +2270,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_recalculate_country_rate(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm._recalculate_total_and_payment_fee()
@@ -2300,6 +2303,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_recalculate_country_rate_keep_gross(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm._recalculate_total_and_payment_fee()
@@ -2330,6 +2334,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_recalculate_reverse_charge(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm._recalculate_total_and_payment_fee()
@@ -2488,6 +2493,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_split_pending_payment_fees(self):
# Set payment fees
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('2.00'))
prov.settings.set('_fee_abs', Decimal('1.00'))
@@ -2691,6 +2697,7 @@ class OrderChangeManagerTests(TestCase):
ia = self._enable_reverse_charge()
# Set payment fees
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('2.00'))
prov.settings.set('_fee_reverse_calc', False)
@@ -2784,6 +2791,7 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_split_paid_payment_fees(self):
# Set payment fees
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('2.00'))
prov.settings.set('_fee_abs', Decimal('1.00'))

View File

@@ -27,11 +27,8 @@ from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope
from pretix.base.models import (
Event, InvoiceAddress, OrderFee, OrderPosition, Organizer, TaxRule,
)
from pretix.base.models import Event, InvoiceAddress, Organizer, TaxRule
from pretix.base.models.tax import TaxedPrice
from pretix.base.services.tax import split_fee_for_taxes
@pytest.fixture
@@ -965,39 +962,3 @@ def test_allow_negative(event):
price_includes_tax=True,
)
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
@pytest.mark.django_db
def test_split_fees(event):
tr19 = TaxRule(rate=Decimal("19.00"), pk=1)
tr7 = TaxRule(rate=Decimal("7.00"), pk=2)
item = event.items.create(name="Budget Ticket", default_price=23)
op1 = OrderPosition(price=Decimal("11.90"), item=item)
op1._calculate_tax(tax_rule=tr19, invoice_address=InvoiceAddress())
op2 = OrderPosition(price=Decimal("10.70"), item=item)
op2._calculate_tax(tax_rule=tr7, invoice_address=InvoiceAddress())
of1 = OrderFee(value=Decimal("5.00"), fee_type=OrderFee.FEE_TYPE_SHIPPING)
of1._calculate_tax(tax_rule=tr7, invoice_address=InvoiceAddress())
# Example of a 10% service fee
assert split_fee_for_taxes([op1, op2], Decimal("2.26"), event) == [
(tr7, Decimal("1.07")),
(tr19, Decimal("1.19")),
]
# Example of a full cancellation fee
assert split_fee_for_taxes([op1, op2], Decimal("22.60"), event) == [
(tr7, Decimal("10.70")),
(tr19, Decimal("11.90")),
]
assert split_fee_for_taxes([op1, op2, of1], Decimal("27.60"), event) == [
(tr7, Decimal("15.70")),
(tr19, Decimal("11.90")),
]
# Example that rounding always is done with benefit to the highest tax rate
assert split_fee_for_taxes([op1, op2], Decimal("0.03"), event) == [
(tr7, Decimal("0.01")),
(tr19, Decimal("0.02")),
]

View File

@@ -443,17 +443,19 @@ class EventsTest(SoupTest):
assert self.event1.settings.get('payment_banktransfer__fee_abs', as_type=Decimal) == Decimal('12.23')
def test_payment_settings(self):
tr19 = self.event1.tax_rules.create(rate=Decimal('19.00'))
self.get_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug))
self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), {
'payment_term_days': '2',
'payment_term_minutes': '30',
'payment_term_mode': 'days',
'tax_rule_payment': 'default',
'tax_rate_default': tr19.pk,
})
self.event1.settings.flush()
assert self.event1.settings.get('payment_term_days', as_type=int) == 2
def test_payment_settings_last_date_payment_after_presale_end(self):
tr19 = self.event1.tax_rules.create(rate=Decimal('19.00'))
self.event1.presale_end = now()
self.event1.save(update_fields=['presale_end'])
doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), {
@@ -462,13 +464,15 @@ class EventsTest(SoupTest):
'payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'),
'payment_term_last_2': '0',
'payment_term_last_3': 'date_from',
'tax_rule_payment': 'default',
'tax_rate_default': tr19.pk,
})
assert doc.select('.alert-danger')
self.event1.presale_end = None
self.event1.save(update_fields=['presale_end'])
def test_payment_settings_relative_date_payment_after_presale_end(self):
with scopes_disabled():
tr19 = self.event1.tax_rules.create(rate=Decimal('19.00'))
self.event1.presale_end = self.event1.date_from - datetime.timedelta(days=5)
self.event1.save(update_fields=['presale_end'])
doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), {
@@ -477,7 +481,7 @@ class EventsTest(SoupTest):
'payment_term_last_1': '',
'payment_term_last_2': '10',
'payment_term_last_3': 'date_from',
'tax_rule_payment': 'default',
'tax_rate_default': tr19.pk,
})
assert doc.select('.alert-danger')
self.event1.presale_end = None
@@ -908,7 +912,7 @@ class EventsTest(SoupTest):
def test_create_event_copy_success(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(
rate=19, name="VAT", default=True
rate=19, name="VAT"
)
q1 = self.event1.quotas.create(
name='Foo',
@@ -919,6 +923,7 @@ class EventsTest(SoupTest):
category=None, default_price=23, tax_rule=tr,
admission=True, hidden_if_available=q1
)
self.event1.settings.tax_rate_default = tr
doc = self.get_doc('/control/events/add')
doc = self.post_doc('/control/events/add', {
@@ -985,13 +990,14 @@ class EventsTest(SoupTest):
def test_create_event_clone_success(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(
rate=19, name="VAT", default=True
rate=19, name="VAT"
)
self.event1.items.create(
name='Early-bird ticket',
category=None, default_price=23, tax_rule=tr,
admission=True
)
self.event1.settings.tax_rate_default = tr
doc = self.get_doc('/control/events/add?clone=' + str(self.event1.pk))
tabletext = doc.select("form")[0].text
self.assertIn("CCC", tabletext)

View File

@@ -439,37 +439,8 @@ def test_order_cancel_paid_keep_fee(client, env):
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
o.event.tax_rules.create(rate=Decimal('7.00'), default=True)
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
assert not o.positions.exists()
assert o.all_positions.exists()
f = o.fees.get()
assert f.fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f.value == Decimal('6.00')
assert f.tax_value == Decimal('0.00')
assert f.tax_rate == Decimal('0.00')
assert f.tax_rule is None
assert o.status == Order.STATUS_PAID
assert o.total == Decimal('6.00')
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_paid_keep_fee_taxed(client, env):
env[0].settings.tax_rule_cancellation = "default"
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'), default=True)
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'))
o.event.settings.tax_rate_default = tr7
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
@@ -491,50 +462,6 @@ def test_order_cancel_paid_keep_fee_taxed(client, env):
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_paid_keep_fee_tax_split(client, env):
env[0].settings.tax_rule_cancellation = "split"
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'), default=False)
tr19 = o.event.tax_rules.create(rate=Decimal('19.00'), default=True)
op1 = o.positions.first()
op1._calculate_tax(tax_rule=tr7)
op1.save()
op2 = o.all_positions.last()
op2.canceled = False
op2._calculate_tax(tax_rule=tr19)
op2.save()
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
assert not o.positions.exists()
assert o.all_positions.exists()
f = o.fees.order_by("-tax_rate")
assert len(f) == 2
assert f[0].fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f[0].value == Decimal('3.00')
assert f[0].tax_value == Decimal('0.48')
assert f[0].tax_rate == Decimal('19')
assert f[0].tax_rule == tr19
assert f[1].fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f[1].value == Decimal('3.00')
assert f[1].tax_value == Decimal('0.20')
assert f[1].tax_rate == Decimal('7')
assert f[1].tax_rule == tr7
assert o.status == Order.STATUS_PAID
assert o.total == Decimal('6.00')
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_pending_keep_fee(client, env):
with scopes_disabled():

View File

@@ -104,7 +104,6 @@ event_urls = [
"settings/tax/add",
"settings/tax/1/",
"settings/tax/1/delete",
"settings/tax/1/default",
"items/",
"items/add",
"items/1/",
@@ -319,7 +318,6 @@ event_permission_urls = [
("can_change_event_settings", "settings/tax/1/", 404, HTTP_GET),
("can_change_event_settings", "settings/tax/add", 200, HTTP_GET),
("can_change_event_settings", "settings/tax/1/delete", 404, HTTP_GET),
("can_change_event_settings", "settings/tax/1/default", 404, HTTP_POST),
("can_change_event_settings", "comment/", 405, HTTP_GET),
# Lists are currently not access-controlled
# ("can_change_items", "items/", 200),

View File

@@ -56,22 +56,9 @@ class TaxRateFormTest(SoupTest):
assert doc.select(".alert-success")
self.assertIn("VAT", doc.select("#page-wrapper table")[0].text)
with scopes_disabled():
tr = self.event1.tax_rules.get(
assert self.event1.tax_rules.get(
rate=19, price_includes_tax=True, eu_reverse_charge=False
)
assert tr.default
def test_set_default(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(rate=19, name="VAT")
tr2 = self.event1.tax_rules.create(rate=7, name="VAT", default=True)
doc = self.post_doc('/control/event/%s/%s/settings/tax/%s/default' % (self.orga1.slug, self.event1.slug, tr.id),
{})
assert doc.select(".alert-success")
tr.refresh_from_db()
assert tr.default
tr2.refresh_from_db()
assert not tr2.default
def test_update(self):
with scopes_disabled():
@@ -111,8 +98,8 @@ class TaxRateFormTest(SoupTest):
def test_delete_default_rule(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(rate=19, name="VAT", default=True)
self.event1.tax_rules.create(rate=7, name="V2")
tr = self.event1.tax_rules.create(rate=19, name="VAT")
self.event1.settings.tax_rate_default = tr
doc = self.get_doc('/control/event/%s/%s/settings/tax/%s/delete' % (self.orga1.slug, self.event1.slug, tr.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
doc = self.post_doc('/control/event/%s/%s/settings/tax/%s/delete' % (self.orga1.slug, self.event1.slug, tr.id),

View File

@@ -19,92 +19,213 @@
# 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/>.
#
import io
import re
import pytest
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from PIL import Image
from pretix.helpers.templatetags.thumb import thumbset
from pretix.helpers.thumb import resize_image
def test_no_resize():
img = Image.new('RGB', (40, 20))
img = resize_image(img, "100x100")
img, limited_by_input = resize_image(img, "100x100")
width, height = img.size
assert limited_by_input
assert width == 40
assert height == 20
img = Image.new('RGB', (40, 20))
img = resize_image(img, "100x100^")
img, limited_by_input = resize_image(img, "100x100^")
width, height = img.size
assert limited_by_input
assert width == 40
assert height == 20
img = Image.new('RGB', (40, 20))
img, limited_by_input = resize_image(img, "40x20^")
width, height = img.size
assert not limited_by_input
assert width == 40
assert height == 20
def test_resize():
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10x10")
img, limited_by_input = resize_image(img, "10x10")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 5
img = Image.new('RGB', (40, 20))
img = resize_image(img, "100x10")
img, limited_by_input = resize_image(img, "100x10")
width, height = img.size
assert not limited_by_input
assert width == 20
assert height == 10
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10x100")
img, limited_by_input = resize_image(img, "10x100")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 5
def test_crop():
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10x10^")
img, limited_by_input = resize_image(img, "10x10^")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 10
img = Image.new('RGB', (40, 20))
img, limited_by_input = resize_image(img, "40x20^")
width, height = img.size
assert not limited_by_input
assert width == 40
assert height == 20
img = Image.new('RGB', (40, 20))
img, limited_by_input = resize_image(img, "50x30^")
width, height = img.size
assert limited_by_input
assert width == 40
assert height == 20
def test_exactsize():
img = Image.new('RGB', (6912, 3456))
img = resize_image(img, "600_x5000")
img, limited_by_input = resize_image(img, "600_x5000")
width, height = img.size
assert not limited_by_input
assert width == 600
assert height == 300
img = Image.new('RGB', (60, 20))
img = resize_image(img, "10_x10")
img, limited_by_input = resize_image(img, "10_x10")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 3
img = Image.new('RGB', (10, 20))
img = resize_image(img, "10_x10")
img, limited_by_input = resize_image(img, "10_x10")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 10
img = Image.new('RGB', (60, 20))
img = resize_image(img, "10x10_")
img, limited_by_input = resize_image(img, "10x10_")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 10
img = Image.new('RGB', (20, 60))
img = resize_image(img, "10x10_")
img, limited_by_input = resize_image(img, "10x10_")
width, height = img.size
assert not limited_by_input
assert width == 3
assert height == 10
img = Image.new('RGB', (20, 60))
img = resize_image(img, "10_x10_")
img, limited_by_input = resize_image(img, "10_x10_")
width, height = img.size
assert not limited_by_input
assert width == 10
assert height == 10
img = Image.new('RGB', (20, 60))
img = resize_image(img, "100_x100_")
img, limited_by_input = resize_image(img, "100_x100_")
width, height = img.size
assert limited_by_input
assert width == 100
assert height == 100
img = Image.new('RGB', (20, 60))
img, limited_by_input = resize_image(img, "20_x60_")
width, height = img.size
assert not limited_by_input
assert width == 20
assert height == 60
def _create_img(size):
img = Image.new('RGB', size)
with io.BytesIO() as output:
img.save(output, format="PNG")
contents = output.getvalue()
return default_storage.save("_".join(str(a) for a in size) + ".png", ContentFile(contents))
@pytest.mark.django_db
def test_thumbset():
# Product picture example
img = _create_img((60, 60))
assert not thumbset(img, "60x60^")
img = _create_img((110, 110))
assert not thumbset(img, "60x60^")
img = _create_img((120, 120))
assert re.match(
r".*\.120x120c\.png 2x$",
thumbset(img, "60x60^"),
)
img = _create_img((150, 150))
assert re.match(
r".*\.120x120c\.png 2x$",
thumbset(img, "60x60^"),
)
img = _create_img((180, 180))
assert re.match(
r".*\.120x120c\.png 2x, .*\.180x180c.png 3x$",
thumbset(img, "60x60^"),
)
img = _create_img((500, 500))
assert re.match(
r".*\.120x120c\.png 2x, .*\.180x180c.png 3x$",
thumbset(img, "60x60^"),
)
# Event logo (large version) example
img = _create_img((400, 200))
assert not thumbset(img, "1170x5000")
img = _create_img((1170, 120))
assert not thumbset(img, "1170x5000")
img = _create_img((2340, 240))
assert re.match(
r".*\.2340x10000\.png 2x$",
thumbset(img, "1170x5000"),
)
img = _create_img((2925, 180))
assert re.match(
r".*\.2340x10000\.png 2x$",
thumbset(img, "1170x5000"),
)
img = _create_img((3510, 360))
assert re.match(
r".*\.2340x10000\.png 2x, .*\.3510x15000.png 3x$",
thumbset(img, "1170x5000"),
)
img = _create_img((4680, 480))
assert re.match(
r".*\.2340x10000\.png 2x, .*\.3510x15000.png 3x$",
thumbset(img, "1170x5000"),
)

View File

@@ -483,7 +483,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert cr1.price == Decimal('23.00')
def test_custom_tax_rules_blocked_on_fee(self):
self.tr7 = self.event.tax_rules.create(rate=7, default=True)
self.tr7 = self.event.tax_rules.create(rate=7)
self.tr7.custom_rules = json.dumps([
{'country': 'AT', 'address_type': 'business_vat_id', 'action': 'reverse'},
{'country': 'ZZ', 'address_type': '', 'action': 'block'},
@@ -492,6 +492,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.set('payment_banktransfer__fee_percent', 20)
self.event.settings.set('payment_banktransfer__fee_reverse_calc', False)
self.event.settings.set('tax_rate_default', self.tr7)
self.event.settings.invoice_address_vatid = True
with scopes_disabled():