2022-09-19 17:33:27 +02:00
|
|
|
from collections import defaultdict
|
2018-02-27 23:12:01 +01:00
|
|
|
from decimal import Decimal
|
|
|
|
from django.dispatch import receiver
|
|
|
|
from django.http import HttpRequest
|
2022-09-19 17:33:51 +02:00
|
|
|
from django.urls import resolve, reverse
|
2024-02-14 11:42:48 +01:00
|
|
|
from django.utils.translation import gettext, gettext_lazy as _
|
2019-04-03 15:38:03 +02:00
|
|
|
from pretix.base.decimal import round_decimal
|
2024-02-14 11:42:48 +01:00
|
|
|
from pretix.base.models import CartPosition, Event, TaxRule
|
2018-02-27 23:12:01 +01:00
|
|
|
from pretix.base.models.orders import OrderFee
|
2022-03-18 14:52:26 +01:00
|
|
|
from pretix.base.settings import settings_hierarkey
|
2018-02-27 23:12:01 +01:00
|
|
|
from pretix.base.signals import order_fee_calculation
|
2018-03-03 21:06:14 +01:00
|
|
|
from pretix.base.templatetags.money import money_filter
|
2018-02-27 23:12:01 +01:00
|
|
|
from pretix.control.signals import nav_event_settings
|
2022-09-19 17:33:51 +02:00
|
|
|
from pretix.presale.signals import (
|
|
|
|
fee_calculation_for_cart, front_page_top, order_meta_from_request,
|
|
|
|
)
|
2019-05-20 14:54:27 +02:00
|
|
|
from pretix.presale.views import get_cart
|
2020-04-01 17:10:19 +02:00
|
|
|
from pretix.presale.views.cart import cart_session
|
2018-02-27 23:12:01 +01:00
|
|
|
|
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
@receiver(nav_event_settings, dispatch_uid="service_fee_nav_settings")
|
2018-02-27 23:12:01 +01:00
|
|
|
def navbar_settings(sender, request, **kwargs):
|
|
|
|
url = resolve(request.path_info)
|
2024-05-31 16:41:47 +02:00
|
|
|
return [
|
|
|
|
{
|
|
|
|
"label": _("Service Fee"),
|
|
|
|
"url": reverse(
|
|
|
|
"plugins:pretix_servicefees:settings",
|
|
|
|
kwargs={
|
|
|
|
"event": request.event.slug,
|
|
|
|
"organizer": request.organizer.slug,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
"active": url.namespace == "plugins:pretix_servicefees"
|
|
|
|
and url.url_name.startswith("settings"),
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def get_fees(
|
|
|
|
event,
|
|
|
|
total,
|
|
|
|
invoice_address,
|
|
|
|
mod="",
|
|
|
|
request=None,
|
|
|
|
positions=[],
|
|
|
|
gift_cards=None,
|
|
|
|
payment_requests=None,
|
|
|
|
):
|
2019-05-20 14:54:27 +02:00
|
|
|
if request is not None and not positions:
|
|
|
|
positions = get_cart(request)
|
2022-03-31 21:19:38 +02:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
skip_free = event.settings.get("service_fee_skip_free", as_type=bool)
|
2022-03-31 21:19:38 +02:00
|
|
|
if skip_free:
|
2024-05-31 16:41:47 +02:00
|
|
|
positions = [pos for pos in positions if pos.price != Decimal("0.00")]
|
2022-03-18 14:53:51 +01:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
skip_addons = event.settings.get("service_fee_skip_addons", as_type=bool)
|
2022-03-18 14:53:51 +01:00
|
|
|
if skip_addons:
|
|
|
|
positions = [pos for pos in positions if not pos.addon_to_id]
|
2019-05-20 14:54:27 +02:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
skip_non_admission = event.settings.get(
|
|
|
|
"service_fee_skip_non_admission", as_type=bool
|
|
|
|
)
|
2022-03-18 14:52:26 +01:00
|
|
|
if skip_non_admission:
|
|
|
|
positions = [pos for pos in positions if pos.item.admission]
|
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_per_ticket = event.settings.get("service_fee_per_ticket" + mod, as_type=Decimal)
|
2019-05-20 14:54:27 +02:00
|
|
|
if mod and fee_per_ticket is None:
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_per_ticket = event.settings.get("service_fee_per_ticket", as_type=Decimal)
|
2019-05-20 14:54:27 +02:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_abs = event.settings.get("service_fee_abs" + mod, as_type=Decimal)
|
2019-04-03 14:58:57 +02:00
|
|
|
if mod and fee_abs is None:
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_abs = event.settings.get("service_fee_abs", as_type=Decimal)
|
2019-04-03 14:58:57 +02:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_percent = event.settings.get("service_fee_percent" + mod, as_type=Decimal)
|
2019-04-03 14:58:57 +02:00
|
|
|
if mod and fee_percent is None:
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_percent = event.settings.get("service_fee_percent", as_type=Decimal)
|
2019-04-03 14:58:57 +02:00
|
|
|
|
2019-05-20 14:54:27 +02:00
|
|
|
fee_per_ticket = Decimal("0") if fee_per_ticket is None else fee_per_ticket
|
2019-04-03 17:06:17 +02:00
|
|
|
fee_abs = Decimal("0") if fee_abs is None else fee_abs
|
|
|
|
fee_percent = Decimal("0") if fee_percent is None else fee_percent
|
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
if event.settings.get("service_fee_skip_if_gift_card", as_type=bool):
|
2022-11-23 14:53:01 +01:00
|
|
|
if payment_requests is not None:
|
|
|
|
for p in payment_requests:
|
2024-05-31 16:41:47 +02:00
|
|
|
if p["provider"] == "giftcard":
|
|
|
|
total = max(0, total - Decimal(p["max_value"] or "0"))
|
2022-11-23 14:53:01 +01:00
|
|
|
|
|
|
|
else:
|
|
|
|
# pretix pre 4.15
|
|
|
|
gift_cards = gift_cards or []
|
|
|
|
if request:
|
|
|
|
cs = cart_session(request)
|
2024-05-31 16:41:47 +02:00
|
|
|
if cs.get("gift_cards"):
|
|
|
|
gift_cards = event.organizer.accepted_gift_cards.filter(
|
|
|
|
pk__in=cs.get("gift_cards"), currency=event.currency
|
|
|
|
)
|
2022-11-23 14:53:01 +01:00
|
|
|
summed = 0
|
|
|
|
for gc in gift_cards:
|
|
|
|
fval = Decimal(gc.value) # TODO: don't require an extra query
|
|
|
|
fval = min(fval, total - summed)
|
|
|
|
if fval > 0:
|
|
|
|
total -= fval
|
|
|
|
summed += fval
|
2020-04-01 17:10:19 +02:00
|
|
|
|
2024-02-15 10:30:19 +01:00
|
|
|
# This hack allows any payment provider to declare a setting service_fee_skip_if_{pprovname} in order to implement
|
|
|
|
# the skipping of service fees when they are paid in their entirety through said payment provider/method.
|
|
|
|
# It is notably used by the KulturPass, which is for all intents and purposes a giftcard but handled like a regular
|
|
|
|
# payment provider.
|
|
|
|
if payment_requests is not None:
|
|
|
|
for p in payment_requests:
|
2024-05-31 16:41:47 +02:00
|
|
|
if event.settings.get(
|
|
|
|
f'service_fee_skip_if_{p["provider"]}', default=False, as_type=bool
|
|
|
|
):
|
|
|
|
total = max(0, total - Decimal(p["max_value"] or "0"))
|
2024-02-15 10:30:19 +01:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
if (fee_per_ticket or fee_abs or fee_percent) and total != Decimal("0.00"):
|
2022-09-19 17:33:27 +02:00
|
|
|
tax_rule_zero = TaxRule.zero()
|
2024-05-31 16:41:47 +02:00
|
|
|
fee = round_decimal(
|
|
|
|
fee_abs + total * (fee_percent / 100) + len(positions) * fee_per_ticket,
|
|
|
|
event.currency,
|
|
|
|
)
|
|
|
|
split_taxes = event.settings.get("service_fee_split_taxes", as_type=bool)
|
2022-09-19 17:33:27 +02:00
|
|
|
if split_taxes:
|
|
|
|
# split taxes based on products ordered
|
2024-05-31 16:41:47 +02:00
|
|
|
d = defaultdict(lambda: Decimal("0.00"))
|
2022-09-19 17:33:27 +02:00
|
|
|
for p in positions:
|
|
|
|
if isinstance(p, CartPosition):
|
|
|
|
tr = p.item.tax_rule
|
|
|
|
else:
|
|
|
|
tr = p.tax_rule
|
|
|
|
d[tr] += p.price - p.tax_value
|
|
|
|
|
|
|
|
base_values = sorted([tuple(t) for t in d.items()], key=lambda t: t[0].rate)
|
|
|
|
sum_base = sum(t[1] for t in base_values)
|
|
|
|
if sum_base:
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_values = [
|
|
|
|
(t[0], round_decimal(fee * t[1] / sum_base, event.currency))
|
|
|
|
for t in base_values
|
|
|
|
]
|
2022-09-19 17:33:27 +02:00
|
|
|
sum_fee = sum(t[1] for t 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:
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_values[0] = (
|
|
|
|
fee_values[0][0],
|
|
|
|
fee_values[0][1] + (fee - sum_fee),
|
|
|
|
)
|
2022-09-19 17:33:27 +02:00
|
|
|
elif sum_fee < fee:
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_values[-1] = (
|
|
|
|
fee_values[-1][0],
|
|
|
|
fee_values[-1][1] + (fee - sum_fee),
|
|
|
|
)
|
2022-09-19 17:33:27 +02:00
|
|
|
else:
|
|
|
|
fee_values = [(event.settings.tax_rate_default or tax_rule_zero, fee)]
|
|
|
|
|
|
|
|
else:
|
|
|
|
fee_values = [(event.settings.tax_rate_default or tax_rule_zero, fee)]
|
|
|
|
|
|
|
|
fees = []
|
|
|
|
for tax_rule, price in fee_values:
|
|
|
|
tax_rule = tax_rule or tax_rule_zero
|
2024-05-31 16:41:47 +02:00
|
|
|
tax = tax_rule.tax(
|
|
|
|
price, invoice_address=invoice_address, base_price_is="gross"
|
|
|
|
)
|
|
|
|
fees.append(
|
|
|
|
OrderFee(
|
|
|
|
fee_type=OrderFee.FEE_TYPE_SERVICE,
|
|
|
|
internal_type="",
|
|
|
|
value=price,
|
|
|
|
tax_rate=tax.rate,
|
2024-08-02 13:47:39 +02:00
|
|
|
tax_code=tax.code,
|
2024-05-31 16:41:47 +02:00
|
|
|
tax_value=tax.tax,
|
|
|
|
tax_rule=tax_rule,
|
|
|
|
)
|
|
|
|
)
|
2022-09-19 17:33:27 +02:00
|
|
|
return fees
|
|
|
|
|
2018-02-27 23:12:01 +01:00
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
@receiver(fee_calculation_for_cart, dispatch_uid="service_fee_calc_cart")
|
|
|
|
def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwargs):
|
2024-05-31 16:41:47 +02:00
|
|
|
mod = ""
|
2018-10-25 14:26:21 +02:00
|
|
|
try:
|
2022-09-19 17:33:51 +02:00
|
|
|
from pretix_resellers.utils import (
|
|
|
|
ResellerException, get_reseller_and_user,
|
|
|
|
)
|
2018-10-25 14:26:21 +02:00
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
try:
|
2020-07-06 16:45:35 +02:00
|
|
|
reseller, user = get_reseller_and_user(request)
|
2020-09-26 19:14:21 +02:00
|
|
|
config = reseller.configs.get(organizer_id=sender.organizer_id)
|
|
|
|
if config.skip_default_service_fees:
|
2020-07-06 16:45:35 +02:00
|
|
|
return []
|
2018-10-25 14:26:21 +02:00
|
|
|
except ResellerException:
|
|
|
|
pass
|
|
|
|
else:
|
2024-05-31 16:41:47 +02:00
|
|
|
mod = "_resellers"
|
|
|
|
return get_fees(
|
|
|
|
sender,
|
|
|
|
total,
|
|
|
|
invoice_address,
|
|
|
|
mod,
|
|
|
|
request,
|
|
|
|
payment_requests=kwargs.get("payment_requests"),
|
|
|
|
)
|
2018-02-27 23:12:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
@receiver(order_fee_calculation, dispatch_uid="service_fee_calc_order")
|
2024-05-31 16:41:47 +02:00
|
|
|
def order_fee(
|
|
|
|
sender: Event, positions, invoice_address, total, meta_info, gift_cards, **kwargs
|
|
|
|
):
|
|
|
|
mod = ""
|
|
|
|
if meta_info.get("servicefees_reseller_id"):
|
|
|
|
mod = "_resellers"
|
2020-07-06 16:45:35 +02:00
|
|
|
try:
|
|
|
|
from pretix_resellers.models import Reseller
|
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
else:
|
2024-05-31 16:41:47 +02:00
|
|
|
r = Reseller.objects.get(pk=meta_info.get("servicefees_reseller_id"))
|
2020-09-26 19:14:21 +02:00
|
|
|
config = r.configs.get(organizer_id=sender.organizer_id)
|
|
|
|
if config.skip_default_service_fees:
|
2020-07-06 16:45:35 +02:00
|
|
|
return []
|
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
return get_fees(
|
|
|
|
sender,
|
|
|
|
total,
|
|
|
|
invoice_address,
|
|
|
|
mod,
|
|
|
|
positions=positions,
|
|
|
|
gift_cards=gift_cards,
|
|
|
|
payment_requests=kwargs.get("payment_requests"),
|
|
|
|
)
|
2018-03-03 21:06:14 +01:00
|
|
|
|
|
|
|
|
|
|
|
@receiver(front_page_top, dispatch_uid="service_fee_front_page_top")
|
|
|
|
def front_page_top_recv(sender: Event, **kwargs):
|
2019-04-03 17:08:41 +02:00
|
|
|
fees = []
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_per_ticket = sender.settings.get("service_fee_per_ticket", as_type=Decimal)
|
2019-05-20 14:54:27 +02:00
|
|
|
if fee_per_ticket:
|
2024-05-31 16:41:47 +02:00
|
|
|
fees = fees + [
|
|
|
|
"{} {}".format(
|
|
|
|
money_filter(fee_per_ticket, sender.currency), gettext("per ticket")
|
|
|
|
)
|
|
|
|
]
|
2019-05-20 14:54:27 +02:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_abs = sender.settings.get("service_fee_abs", as_type=Decimal)
|
2019-04-03 17:08:41 +02:00
|
|
|
if fee_abs:
|
2024-05-31 16:41:47 +02:00
|
|
|
fees = fees + [
|
|
|
|
"{} {}".format(money_filter(fee_abs, sender.currency), gettext("per order"))
|
|
|
|
]
|
2019-04-03 17:08:41 +02:00
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
fee_percent = sender.settings.get("service_fee_percent", as_type=Decimal)
|
2019-04-03 17:08:41 +02:00
|
|
|
if fee_percent:
|
2024-05-31 16:41:47 +02:00
|
|
|
fees = fees + ["{} % {}".format(fee_percent, gettext("per order"))]
|
2019-04-03 17:08:41 +02:00
|
|
|
|
2019-05-20 14:54:27 +02:00
|
|
|
if fee_per_ticket or fee_abs or fee_percent:
|
2024-05-31 16:41:47 +02:00
|
|
|
return "<p>%s</p>" % gettext(
|
|
|
|
"A service fee of {} will be added to the order total."
|
|
|
|
).format(" {} ".format(gettext("plus")).join(fees))
|
2018-10-25 14:26:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
@receiver(order_meta_from_request, dispatch_uid="servicefees_order_meta")
|
|
|
|
def order_meta_signal(sender: Event, request: HttpRequest, **kwargs):
|
|
|
|
meta = {}
|
|
|
|
try:
|
2022-09-19 17:33:51 +02:00
|
|
|
from pretix_resellers.utils import (
|
|
|
|
ResellerException, get_reseller_and_user,
|
|
|
|
)
|
2018-10-25 14:26:21 +02:00
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
try:
|
2020-07-06 16:45:35 +02:00
|
|
|
reseller, user = get_reseller_and_user(request)
|
2024-05-31 16:41:47 +02:00
|
|
|
meta["servicefees_reseller_id"] = reseller.pk
|
2018-10-25 14:26:21 +02:00
|
|
|
except ResellerException:
|
|
|
|
pass
|
|
|
|
return meta
|
2022-03-18 14:53:51 +01:00
|
|
|
|
|
|
|
|
2024-05-31 16:41:47 +02:00
|
|
|
settings_hierarkey.add_default("service_fee_skip_addons", "True", bool)
|
|
|
|
settings_hierarkey.add_default("service_fee_skip_free", "True", bool)
|