forked from CGM_Public/pretix_original
Compare commits
8 Commits
v2025.6.0
...
fix-widget
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cbf82700 | ||
|
|
19a7042c16 | ||
|
|
14ed6982a5 | ||
|
|
090358833d | ||
|
|
f0212d910d | ||
|
|
a4c74f6310 | ||
|
|
f66a41f6a7 | ||
|
|
1a990dfecc |
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
24
src/pretix/base/migrations/0282_taxrule_default.py
Normal file
24
src/pretix/base/migrations/0282_taxrule_default.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
52
src/pretix/base/views/webmanifest.py
Normal file
52
src/pretix/base/views/webmanifest.py
Normal 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'
|
||||
)
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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> </p>
|
||||
<div class="alert alert-info" id="version-notice">
|
||||
{% blocktrans trimmed with print_version="2.18" scan_version="1.22" %}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user