Compare commits

..

8 Commits

Author SHA1 Message Date
Richard Schreiber
f2cbf82700 Fix broken widget cache 2025-07-01 11:08:46 +02:00
Raphael Michel
19a7042c16 Fix migration for large databases 2025-06-30 19:45:46 +02:00
Raphael Michel
14ed6982a5 New data model for default tax rule and new options for cancellation fees (#4962)
* New data model for default tax rule

* Remove misleading empty label when field is not optional

* Allow to split cancellation fee

* Fix API and tests

* Update migration

* Update src/tests/api/test_taxrules.py

Co-authored-by: luelista <weller@rami.io>

* Update src/tests/api/test_taxrules.py

Co-authored-by: luelista <weller@rami.io>

* Review note

* Update src/pretix/base/models/tax.py

Co-authored-by: luelista <weller@rami.io>

* Flip API behaviour for default

* Fix failing tests

* Fix failing test

* Split migration

---------

Co-authored-by: luelista <weller@rami.io>
2025-06-30 16:47:09 +02:00
Richard Schreiber
090358833d Remove browserconfig.xml (#5280)
* Remove meta-elements

* remove url-route
2025-06-30 11:25:18 +02:00
Raphael Michel
f0212d910d Widget: Make table stripe colors background-agnostic (#5277) 2025-06-30 11:20:14 +02:00
Richard Schreiber
a4c74f6310 PDF-Editor: use panel-head as topbar for common commands/tools and preview/save (#4977) 2025-06-30 11:19:39 +02:00
Richard Schreiber
f66a41f6a7 Presale: remove webmanifest (#5275)
* Remove webmanifest from presale

* move webmanifest from presale to base urls
2025-06-30 09:33:42 +02:00
Raphael Michel
1a990dfecc Bump version to 2025.7.0.dev0 2025-06-27 09:28:21 +02:00
47 changed files with 874 additions and 318 deletions

View File

@@ -26,6 +26,8 @@ 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.
@@ -48,6 +50,10 @@ 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
@@ -111,6 +117,7 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"default": true,
"internal_name": "VAT",
"code": "S/standard",
"rate": "19.00",
@@ -153,6 +160,7 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"default": true,
"internal_name": "VAT",
"code": "S/standard",
"rate": "19.00",
@@ -203,6 +211,7 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"default": false,
"internal_name": "VAT",
"code": "S/standard",
"rate": "19.00",

View File

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

View File

@@ -685,8 +685,26 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
class Meta:
model = TaxRule
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
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
class EventSettingsSerializer(SettingsSerializer):
@@ -712,6 +730,8 @@ 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',
@@ -942,6 +962,8 @@ 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,6 +580,11 @@ 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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,60 @@
# 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,13 +1113,6 @@ 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]
@@ -1198,6 +1191,10 @@ 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):
def _calculate_tax(self, tax_rule=None, invoice_address=None):
if tax_rule:
self.tax_rule = tax_rule
try:
ia = self.order.invoice_address
ia = invoice_address or 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_rate_default:
self.tax_rule = self.order.event.settings.tax_rate_default
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 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,9 +377,20 @@ 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
@@ -394,7 +405,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 self.event.settings.tax_rate_default != self
and not (self.default and self.event.tax_rules.filter(~models.Q(pk=self.pk)).exists())
)
@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, User, WaitingListEntry,
SubEvent, TaxRule, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, mail
@@ -40,6 +40,7 @@ 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
@@ -268,14 +269,34 @@ 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:
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=fee,
order=o,
tax_rule=o.event.settings.tax_rate_default,
)
f._calculate_tax()
ocm.add_fee(f)
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,
order=o,
tax_rate=tax.rate,
tax_code=tax.code,
tax_value=tax.tax,
tax_rule=tax_rule,
)
ocm.add_fee(f)
ocm.commit()
refund_amount = o.payment_refund_sum - o.total

View File

@@ -1534,7 +1534,10 @@ def get_fees(event, request, total, invoice_address, payments, positions):
total_remaining -= to_pay
if payment_fee:
payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero()
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 = 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,6 +62,7 @@ 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
@@ -96,6 +97,7 @@ 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,
@@ -486,7 +488,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):
cancellation_fee=None, keep_fees=None, cancel_invoice=True, comment=None, tax_mode=None):
"""
Mark this order as canceled
:param order: The order to change
@@ -506,6 +508,10 @@ 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.'))
@@ -533,7 +539,9 @@ 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
@@ -546,18 +554,39 @@ 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:
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=new_fee,
tax_rule=order.event.settings.tax_rate_default,
order=order,
)
f._calculate_tax()
f.save()
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,
order=order,
tax_rate=tax.rate,
tax_code=tax.code,
tax_value=tax.tax,
tax_rule=tax_rule,
)
f.save()
if cancellation_fee > order.total:
raise OrderError(_('The cancellation fee cannot be higher than the total amount of this order.'))

View File

@@ -22,6 +22,8 @@
import logging
import os
import re
from collections import defaultdict
from decimal import Decimal
from xml.etree import ElementTree
import requests
@@ -32,7 +34,9 @@ from zeep import Client, Transport
from zeep.cache import SqliteCache
from zeep.exceptions import Fault
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
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
logger = logging.getLogger(__name__)
error_messages = {
@@ -229,3 +233,64 @@ 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, TaxRule
from pretix.base.models.tax import VAT_ID_COUNTRIES
from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
SerializerRelativeDateField, SerializerRelativeDateTimeField,
@@ -1027,9 +1027,47 @@ DEFAULTS = {
widget=forms.CheckboxInput,
)
},
'tax_rate_default': {
'default': None,
'type': TaxRule
'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')),
),
)
},
'invoice_generate': {
'default': 'False',

View File

@@ -0,0 +1,52 @@
#
# 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,6 +761,7 @@ class CancelSettingsForm(SettingsForm):
'change_allow_user_addons',
'change_allow_user_if_checked_in',
'change_allow_attendee',
'tax_rule_cancellation',
]
def __init__(self, *args, **kwargs):
@@ -783,14 +784,8 @@ 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')
@@ -804,10 +799,6 @@ 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,7 +406,6 @@ 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(),
@@ -416,6 +415,8 @@ 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,8 +174,7 @@ 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. Please always enter a gross value, '
'tax will be calculated automatically.'),
'in your cancellation fee if you want to keep them.'),
)
cancel_invoice = forms.BooleanField(
label=_('Generate cancellation for invoice'),
@@ -200,6 +199,19 @@ 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,10 +20,8 @@
{% 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 "presale:site.webmanifest" %}">
<link rel="manifest" href="{% url "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,10 +79,8 @@
{% 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 "presale:site.webmanifest" %}">
<link rel="manifest" href="{% url "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,6 +26,7 @@
{% 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_rate_default layout="control" %}
{% bootstrap_field form.tax_rule_payment layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %}
</fieldset>
</div>

View File

@@ -9,11 +9,10 @@
{% 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 or has been in use for any existing orders.{% endblocktrans %}</p>
<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>
{% 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,6 +24,7 @@
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Default" %}</th>
<th>{% trans "Rate" %}</th>
<th class="action-col-2"></th>
</tr>
@@ -36,6 +37,22 @@
{{ 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,5 +1,6 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load icon %}
{% load static %}
{% load compress %}
{% block title %}{% trans "PDF Editor" %}{% endblock %}
@@ -25,27 +26,102 @@
<div class="col-md-9">
<div class="panel panel-default panel-pdf-editor">
<div class="panel-heading">
<div class="pull-right flip">
<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="btn-group">
<button type="button" class="btn btn-default btn-xs" id="toolbox-source"
<button type="button" class="btn btn-default" id="toolbox-source"
title="{% trans "Code" %}">
<span class="fa fa-code"></span>
{% icon "code" %}
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-paste"
title="{% trans "Paste" %}">
<span class="fa fa-paste"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-undo"
title="{% trans "Undo" %}">
<span class="fa fa-undo"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-redo"
title="{% trans "Redo" %}">
<span class="fa fa-repeat"></span>
<button type="submit" class="btn btn-default" id="editor-preview">
{% icon "eye" %}
{% trans "Preview" %}
</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"
title="{% trans "Undo" %}">
{% icon "undo" %}
</button>
<button type="button" class="btn btn-default" id="toolbox-redo"
title="{% trans "Redo" %}">
{% icon "repeat" %}
</button>
</div>
{% trans "Editor" %}
</div>
<div class="panel-body">
<ul class="nav nav-pills" id="page_nav">
@@ -153,22 +229,6 @@
<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>
@@ -488,63 +548,6 @@
</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,6 +288,7 @@ 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 FormView, ListView
from django.views.generic import DetailView, FormView, ListView
from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString
@@ -1274,6 +1274,8 @@ 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
@@ -1354,6 +1356,50 @@ 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,8 +181,9 @@ class EventWizard(SafeSessionWizardView):
initial['location'] = self.clone_from.location
initial['timezone'] = self.clone_from.settings.timezone
initial['locale'] = self.clone_from.settings.locale
if self.clone_from.settings.tax_rate_default:
initial['tax_rate'] = self.clone_from.settings.tax_rate_default.rate
tax_rule = self.clone_from.cached_default_tax_rule
if tax_rule:
initial['tax_rate'] = tax_rule.rate
if 'organizer' in self.request.GET:
if step == 'foundation':
try:
@@ -325,10 +326,17 @@ class EventWizard(SafeSessionWizardView):
event.set_defaults()
if basics_data['tax_rate'] is not None:
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(
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(
name=LazyI18nString.from_gettext(gettext('VAT')),
rate=basics_data['tax_rate']
rate=basics_data['tax_rate'],
default=not default_tax_rule,
)
event.settings.set('timezone', basics_data['timezone'])

View File

@@ -37,9 +37,6 @@
<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

@@ -235,7 +235,5 @@ 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,9 +24,7 @@ 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
@@ -44,53 +42,6 @@ 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_{}_{}'.format(version, lang))
fname = gs.settings.get('widget_file_v{}_{}'.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_{}_{}'.format(version, lang), 'file://' + newname)
gs.settings.set('widget_checksum_{}_{}'.format(version, lang), checksum)
gs.settings.set('widget_file_v{}_{}'.format(version, lang), 'file://' + newname)
gs.settings.set('widget_checksum_v{}_{}'.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,6 +898,7 @@ 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"));
@@ -1048,8 +1049,11 @@ var editor = {
},
_cut: function () {
editor._history_modification_in_progress = true;
var thing = editor.fabric.getActiveObject();
if (!thing) {
return false;
}
editor._history_modification_in_progress = true;
if (thing.type === "activeSelection") {
editor.clipboard = editor.dump(thing._objects);
thing.forEachObject(function (o) {
@@ -1066,15 +1070,16 @@ 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]);
}
editor._history_modification_in_progress = false;
editor._create_savepoint();
return true;
},
_paste: function () {
@@ -1098,8 +1103,19 @@ 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);
@@ -1119,64 +1135,79 @@ var editor = {
if ($("#source-container").is(':visible')) {
return true;
}
switch (e.keyCode) {
case 38: /* Up arrow */
thing.set('top', thing.get('top') - step);
thing.setCoords();
editor._create_savepoint();
break;
case 40: /* Down arrow */
thing.set('top', thing.get('top') + step);
thing.setCoords();
editor._create_savepoint();
break;
case 37: /* Left arrow */
thing.set('left', thing.get('left') - step);
thing.setCoords();
editor._create_savepoint();
break;
case 39: /* Right arrow */
thing.set('left', thing.get('left') + step);
thing.setCoords();
editor._create_savepoint();
break;
case 8: /* Backspace */
case 46: /* Delete */
editor._delete();
break;
case 65: /* A */
if (e.ctrlKey || e.metaKey) {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case "a":
editor._selectAll();
}
break;
case 89: /* Y */
if (e.ctrlKey || e.metaKey) {
break;
case "y":
editor._redo();
}
break;
case 90: /* Z */
if (e.ctrlKey || e.metaKey) {
break;
case "z":
editor._undo();
}
break;
case 88: /* X */
if (e.ctrlKey || e.metaKey) {
break;
case "x":
editor._cut();
}
break;
case 86: /* V */
if (e.ctrlKey || e.metaKey) {
break;
case "v":
editor._paste();
}
break;
case 67: /* C */
if (e.ctrlKey || e.metaKey) {
break;
case "c":
editor._copy();
}
break;
default:
return;
break;
case "d":
editor._duplicate();
break;
default:
return;
}
} else {
switch (e.key) {
case "ArrowUp":
thing.set('top', thing.get('top') - step);
thing.setCoords();
editor._create_savepoint();
break;
case "ArrowDown":
thing.set('top', thing.get('top') + step);
thing.setCoords();
editor._create_savepoint();
break;
case "ArrowLeft":
thing.set('left', thing.get('left') - step);
thing.setCoords();
editor._create_savepoint();
break;
case "ArrowRight":
thing.set('left', thing.get('left') + step);
thing.setCoords();
editor._create_savepoint();
break;
case "Backspace":
case "Del":
case "Delete":
editor._delete();
break;
case "Cut":
editor._cut();
break;
case "Copy":
editor._copy();
break;
case "Paste":
editor._paste();
break;
case "Redo":
editor._redo();
break;
case "Undo":
editor._undo();
break;
default:
return;
}
}
e.preventDefault();
editor.fabric.renderAll();
editor._update_toolbox_values();
@@ -1234,6 +1265,9 @@ 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 () {
@@ -1417,10 +1451,8 @@ var editor = {
editor._create_savepoint();
});
$("#toolbox .colorpickerfield").bind('changeColor', editor._update_values_from_toolbox);
$("#toolbox-copy").bind('click', editor._copy);
$("#toolbox-cut").bind('click', editor._cut);
$("#toolbox-duplicate").bind('click', editor._duplicate);
$("#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,8 +21,7 @@ body {
#toolbox .text,
#toolbox .textarea,
#toolbox .textcontainer,
#toolbox .imagecontent,
#toolbox .object-buttons {
#toolbox .imagecontent {
display: none;
}
#toolbox[data-type] .position,
@@ -37,8 +36,7 @@ body {
#toolbox[data-type=textcontainer] .textcontainer,
#toolbox[data-type=textcontainer] .rectsize,
#toolbox[data-type=textarea] .text,
#toolbox[data-type=textarea] .textarea,
#toolbox[data-type] .object-buttons {
#toolbox[data-type=textarea] .textarea {
display: block;
}
#toolbox[data-type=text] .btn-group-justified > .btn-group.text,
@@ -92,3 +90,16 @@ 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,6 +3,9 @@
@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,6 +43,7 @@ from pretix.base.views import applepay, js_helpers
from .base.views import (
cachedfiles, csp, health, js_catalog, metrics, redirect, source,
webmanifest,
)
base_patterns = [
@@ -51,6 +52,7 @@ 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")
return event.tax_rules.create(name="VAT", rate=19, code="S/standard", default=True)
@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")
return event.tax_rules.create(rate=Decimal('19.00'), code="S/standard", default=True)
@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_rate_default = taxrule
event.settings.tax_rule_cancellation = "default"
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/{}/mark_canceled/'.format(
organizer.slug, event.slug, order.code

View File

@@ -31,6 +31,7 @@ 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,
@@ -80,6 +81,45 @@ 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'))
tr7 = event1.tax_rules.create(rate=Decimal('7.00'), default=True)
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,7 +2228,6 @@ 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={
@@ -2271,7 +2270,7 @@ class EventTest(TestCase):
assert que1new.type == que1.type
assert que1new.items.get(pk=i1new.pk)
assert event2.settings.foo_setting == '23'
assert event2.settings.tax_rate_default == trnew
assert event2.cached_default_tax_rule == 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=50)
cancel_order(self.order.pk, cancellation_fee=Decimal("50.00"))
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=2.5)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.50"))
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=2.5)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.50"))
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=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), 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=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), 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=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), 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=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), 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=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), 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'))
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'), default=True)
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,7 +1868,6 @@ 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'))
@@ -1882,7 +1881,6 @@ 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()
@@ -2270,7 +2268,6 @@ 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()
@@ -2303,7 +2300,6 @@ 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()
@@ -2334,7 +2330,6 @@ 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()
@@ -2493,7 +2488,6 @@ 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'))
@@ -2697,7 +2691,6 @@ 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)
@@ -2791,7 +2784,6 @@ 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,8 +27,11 @@ from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope
from pretix.base.models import Event, InvoiceAddress, Organizer, TaxRule
from pretix.base.models import (
Event, InvoiceAddress, OrderFee, OrderPosition, Organizer, TaxRule,
)
from pretix.base.models.tax import TaxedPrice
from pretix.base.services.tax import split_fee_for_taxes
@pytest.fixture
@@ -962,3 +965,39 @@ 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,19 +443,17 @@ 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_rate_default': tr19.pk,
'tax_rule_payment': 'default',
})
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), {
@@ -464,15 +462,13 @@ 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_rate_default': tr19.pk,
'tax_rule_payment': 'default',
})
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), {
@@ -481,7 +477,7 @@ class EventsTest(SoupTest):
'payment_term_last_1': '',
'payment_term_last_2': '10',
'payment_term_last_3': 'date_from',
'tax_rate_default': tr19.pk,
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
self.event1.presale_end = None
@@ -912,7 +908,7 @@ class EventsTest(SoupTest):
def test_create_event_copy_success(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(
rate=19, name="VAT"
rate=19, name="VAT", default=True
)
q1 = self.event1.quotas.create(
name='Foo',
@@ -923,7 +919,6 @@ 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', {
@@ -990,14 +985,13 @@ class EventsTest(SoupTest):
def test_create_event_clone_success(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(
rate=19, name="VAT"
rate=19, name="VAT", default=True
)
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,8 +439,37 @@ 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()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'))
o.event.settings.tax_rate_default = tr7
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)
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', {
@@ -462,6 +491,50 @@ def test_order_cancel_paid_keep_fee(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,6 +104,7 @@ event_urls = [
"settings/tax/add",
"settings/tax/1/",
"settings/tax/1/delete",
"settings/tax/1/default",
"items/",
"items/add",
"items/1/",
@@ -318,6 +319,7 @@ 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,9 +56,22 @@ class TaxRateFormTest(SoupTest):
assert doc.select(".alert-success")
self.assertIn("VAT", doc.select("#page-wrapper table")[0].text)
with scopes_disabled():
assert self.event1.tax_rules.get(
tr = 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():
@@ -98,8 +111,8 @@ class TaxRateFormTest(SoupTest):
def test_delete_default_rule(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(rate=19, name="VAT")
self.event1.settings.tax_rate_default = tr
tr = self.event1.tax_rules.create(rate=19, name="VAT", default=True)
self.event1.tax_rules.create(rate=7, name="V2")
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

@@ -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)
self.tr7 = self.event.tax_rules.create(rate=7, default=True)
self.tr7.custom_rules = json.dumps([
{'country': 'AT', 'address_type': 'business_vat_id', 'action': 'reverse'},
{'country': 'ZZ', 'address_type': '', 'action': 'block'},
@@ -492,7 +492,6 @@ 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():