Run black

This commit is contained in:
Raphael Michel 2024-05-31 16:41:47 +02:00
parent a2b7462d0a
commit 063a8e44e9
4 changed files with 199 additions and 122 deletions

View File

@ -1,22 +1,23 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
from . import __version__ from . import __version__
class PluginApp(AppConfig): class PluginApp(AppConfig):
name = 'pretix_servicefees' name = "pretix_servicefees"
verbose_name = 'Service Fees' verbose_name = "Service Fees"
class PretixPluginMeta: class PretixPluginMeta:
name = gettext_lazy('Service Fees') name = gettext_lazy("Service Fees")
author = 'Raphael Michel' author = "Raphael Michel"
category = 'FEATURE' category = "FEATURE"
description = gettext_lazy('This plugin allows to charge a service fee on all non-free orders.') description = gettext_lazy(
"This plugin allows to charge a service fee on all non-free orders."
)
visible = True visible = True
version = __version__ version = __version__
compatibility = "pretix>=4.16.0" compatibility = "pretix>=4.16.0"
def ready(self): def ready(self):
from . import signals # NOQA from . import signals # NOQA

View File

@ -18,64 +18,83 @@ from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session from pretix.presale.views.cart import cart_session
@receiver(nav_event_settings, dispatch_uid='service_fee_nav_settings') @receiver(nav_event_settings, dispatch_uid="service_fee_nav_settings")
def navbar_settings(sender, request, **kwargs): def navbar_settings(sender, request, **kwargs):
url = resolve(request.path_info) url = resolve(request.path_info)
return [{ return [
'label': _('Service Fee'), {
'url': reverse('plugins:pretix_servicefees:settings', kwargs={ "label": _("Service Fee"),
'event': request.event.slug, "url": reverse(
'organizer': request.organizer.slug, "plugins:pretix_servicefees:settings",
}), kwargs={
'active': url.namespace == 'plugins:pretix_servicefees' and url.url_name.startswith('settings'), "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): def get_fees(
event,
total,
invoice_address,
mod="",
request=None,
positions=[],
gift_cards=None,
payment_requests=None,
):
if request is not None and not positions: if request is not None and not positions:
positions = get_cart(request) positions = get_cart(request)
skip_free = event.settings.get('service_fee_skip_free', as_type=bool) skip_free = event.settings.get("service_fee_skip_free", as_type=bool)
if skip_free: if skip_free:
positions = [pos for pos in positions if pos.price != Decimal('0.00')] positions = [pos for pos in positions if pos.price != Decimal("0.00")]
skip_addons = event.settings.get('service_fee_skip_addons', as_type=bool) skip_addons = event.settings.get("service_fee_skip_addons", as_type=bool)
if skip_addons: if skip_addons:
positions = [pos for pos in positions if not pos.addon_to_id] positions = [pos for pos in positions if not pos.addon_to_id]
skip_non_admission = event.settings.get('service_fee_skip_non_admission', as_type=bool) skip_non_admission = event.settings.get(
"service_fee_skip_non_admission", as_type=bool
)
if skip_non_admission: if skip_non_admission:
positions = [pos for pos in positions if pos.item.admission] positions = [pos for pos in positions if pos.item.admission]
fee_per_ticket = event.settings.get('service_fee_per_ticket' + mod, as_type=Decimal) fee_per_ticket = event.settings.get("service_fee_per_ticket" + mod, as_type=Decimal)
if mod and fee_per_ticket is None: if mod and fee_per_ticket is None:
fee_per_ticket = event.settings.get('service_fee_per_ticket', as_type=Decimal) fee_per_ticket = event.settings.get("service_fee_per_ticket", as_type=Decimal)
fee_abs = event.settings.get('service_fee_abs' + mod, as_type=Decimal) fee_abs = event.settings.get("service_fee_abs" + mod, as_type=Decimal)
if mod and fee_abs is None: if mod and fee_abs is None:
fee_abs = event.settings.get('service_fee_abs', as_type=Decimal) fee_abs = event.settings.get("service_fee_abs", as_type=Decimal)
fee_percent = event.settings.get('service_fee_percent' + mod, as_type=Decimal) fee_percent = event.settings.get("service_fee_percent" + mod, as_type=Decimal)
if mod and fee_percent is None: if mod and fee_percent is None:
fee_percent = event.settings.get('service_fee_percent', as_type=Decimal) fee_percent = event.settings.get("service_fee_percent", as_type=Decimal)
fee_per_ticket = Decimal("0") if fee_per_ticket is None else fee_per_ticket fee_per_ticket = Decimal("0") if fee_per_ticket is None else fee_per_ticket
fee_abs = Decimal("0") if fee_abs is None else fee_abs fee_abs = Decimal("0") if fee_abs is None else fee_abs
fee_percent = Decimal("0") if fee_percent is None else fee_percent fee_percent = Decimal("0") if fee_percent is None else fee_percent
if event.settings.get('service_fee_skip_if_gift_card', as_type=bool): if event.settings.get("service_fee_skip_if_gift_card", as_type=bool):
if payment_requests is not None: if payment_requests is not None:
for p in payment_requests: for p in payment_requests:
if p['provider'] == 'giftcard': if p["provider"] == "giftcard":
total = max(0, total - Decimal(p['max_value'] or '0')) total = max(0, total - Decimal(p["max_value"] or "0"))
else: else:
# pretix pre 4.15 # pretix pre 4.15
gift_cards = gift_cards or [] gift_cards = gift_cards or []
if request: if request:
cs = cart_session(request) cs = cart_session(request)
if cs.get('gift_cards'): if cs.get("gift_cards"):
gift_cards = event.organizer.accepted_gift_cards.filter(pk__in=cs.get('gift_cards'), currency=event.currency) gift_cards = event.organizer.accepted_gift_cards.filter(
pk__in=cs.get("gift_cards"), currency=event.currency
)
summed = 0 summed = 0
for gc in gift_cards: for gc in gift_cards:
fval = Decimal(gc.value) # TODO: don't require an extra query fval = Decimal(gc.value) # TODO: don't require an extra query
@ -90,16 +109,21 @@ def get_fees(event, total, invoice_address, mod='', request=None, positions=[],
# payment provider. # payment provider.
if payment_requests is not None: if payment_requests is not None:
for p in payment_requests: for p in payment_requests:
if event.settings.get(f'service_fee_skip_if_{p["provider"]}', default=False, as_type=bool): if event.settings.get(
total = max(0, total - Decimal(p['max_value'] or '0')) f'service_fee_skip_if_{p["provider"]}', default=False, as_type=bool
):
total = max(0, total - Decimal(p["max_value"] or "0"))
if (fee_per_ticket or fee_abs or fee_percent) and total != Decimal('0.00'): if (fee_per_ticket or fee_abs or fee_percent) and total != Decimal("0.00"):
tax_rule_zero = TaxRule.zero() tax_rule_zero = TaxRule.zero()
fee = round_decimal(fee_abs + total * (fee_percent / 100) + len(positions) * fee_per_ticket, event.currency) fee = round_decimal(
split_taxes = event.settings.get('service_fee_split_taxes', as_type=bool) 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)
if split_taxes: if split_taxes:
# split taxes based on products ordered # split taxes based on products ordered
d = defaultdict(lambda: Decimal('0.00')) d = defaultdict(lambda: Decimal("0.00"))
for p in positions: for p in positions:
if isinstance(p, CartPosition): if isinstance(p, CartPosition):
tr = p.item.tax_rule tr = p.item.tax_rule
@ -110,16 +134,24 @@ def get_fees(event, total, invoice_address, mod='', request=None, positions=[],
base_values = sorted([tuple(t) for t in d.items()], key=lambda t: t[0].rate) 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) sum_base = sum(t[1] for t in base_values)
if sum_base: if sum_base:
fee_values = [(t[0], round_decimal(fee * t[1] / sum_base, event.currency)) fee_values = [
for t in base_values] (t[0], round_decimal(fee * t[1] / sum_base, event.currency))
for t in base_values
]
sum_fee = sum(t[1] for t in fee_values) 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 # If there are rounding differences, we fix them up, but always leaning to the benefit of the tax
# authorities # authorities
if sum_fee > fee: if sum_fee > fee:
fee_values[0] = (fee_values[0][0], fee_values[0][1] + (fee - sum_fee)) fee_values[0] = (
fee_values[0][0],
fee_values[0][1] + (fee - sum_fee),
)
elif sum_fee < fee: elif sum_fee < fee:
fee_values[-1] = (fee_values[-1][0], fee_values[-1][1] + (fee - sum_fee)) fee_values[-1] = (
fee_values[-1][0],
fee_values[-1][1] + (fee - sum_fee),
)
else: else:
fee_values = [(event.settings.tax_rate_default or tax_rule_zero, fee)] fee_values = [(event.settings.tax_rate_default or tax_rule_zero, fee)]
@ -129,15 +161,19 @@ def get_fees(event, total, invoice_address, mod='', request=None, positions=[],
fees = [] fees = []
for tax_rule, price in fee_values: for tax_rule, price in fee_values:
tax_rule = tax_rule or tax_rule_zero tax_rule = tax_rule or tax_rule_zero
tax = tax_rule.tax(price, invoice_address=invoice_address, base_price_is='gross') tax = tax_rule.tax(
fees.append(OrderFee( price, invoice_address=invoice_address, base_price_is="gross"
fee_type=OrderFee.FEE_TYPE_SERVICE, )
internal_type='', fees.append(
value=price, OrderFee(
tax_rate=tax.rate, fee_type=OrderFee.FEE_TYPE_SERVICE,
tax_value=tax.tax, internal_type="",
tax_rule=tax_rule value=price,
)) tax_rate=tax.rate,
tax_value=tax.tax,
tax_rule=tax_rule,
)
)
return fees return fees
return [] return []
@ -145,7 +181,7 @@ def get_fees(event, total, invoice_address, mod='', request=None, positions=[],
@receiver(fee_calculation_for_cart, dispatch_uid="service_fee_calc_cart") @receiver(fee_calculation_for_cart, dispatch_uid="service_fee_calc_cart")
def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwargs): def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwargs):
mod = '' mod = ""
try: try:
from pretix_resellers.utils import ( from pretix_resellers.utils import (
ResellerException, get_reseller_and_user, ResellerException, get_reseller_and_user,
@ -161,48 +197,70 @@ def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwar
except ResellerException: except ResellerException:
pass pass
else: else:
mod = '_resellers' mod = "_resellers"
return get_fees(sender, total, invoice_address, mod, request, payment_requests=kwargs.get('payment_requests')) return get_fees(
sender,
total,
invoice_address,
mod,
request,
payment_requests=kwargs.get("payment_requests"),
)
@receiver(order_fee_calculation, dispatch_uid="service_fee_calc_order") @receiver(order_fee_calculation, dispatch_uid="service_fee_calc_order")
def order_fee(sender: Event, positions, invoice_address, total, meta_info, gift_cards, **kwargs): def order_fee(
mod = '' sender: Event, positions, invoice_address, total, meta_info, gift_cards, **kwargs
if meta_info.get('servicefees_reseller_id'): ):
mod = '_resellers' mod = ""
if meta_info.get("servicefees_reseller_id"):
mod = "_resellers"
try: try:
from pretix_resellers.models import Reseller from pretix_resellers.models import Reseller
except ImportError: except ImportError:
pass pass
else: else:
r = Reseller.objects.get(pk=meta_info.get('servicefees_reseller_id')) r = Reseller.objects.get(pk=meta_info.get("servicefees_reseller_id"))
config = r.configs.get(organizer_id=sender.organizer_id) config = r.configs.get(organizer_id=sender.organizer_id)
if config.skip_default_service_fees: if config.skip_default_service_fees:
return [] return []
return get_fees(sender, total, invoice_address, mod, positions=positions, gift_cards=gift_cards, return get_fees(
payment_requests=kwargs.get('payment_requests')) sender,
total,
invoice_address,
mod,
positions=positions,
gift_cards=gift_cards,
payment_requests=kwargs.get("payment_requests"),
)
@receiver(front_page_top, dispatch_uid="service_fee_front_page_top") @receiver(front_page_top, dispatch_uid="service_fee_front_page_top")
def front_page_top_recv(sender: Event, **kwargs): def front_page_top_recv(sender: Event, **kwargs):
fees = [] fees = []
fee_per_ticket = sender.settings.get('service_fee_per_ticket', as_type=Decimal) fee_per_ticket = sender.settings.get("service_fee_per_ticket", as_type=Decimal)
if fee_per_ticket: if fee_per_ticket:
fees = fees + ["{} {}".format(money_filter(fee_per_ticket, sender.currency), gettext('per ticket'))] fees = fees + [
"{} {}".format(
money_filter(fee_per_ticket, sender.currency), gettext("per ticket")
)
]
fee_abs = sender.settings.get('service_fee_abs', as_type=Decimal) fee_abs = sender.settings.get("service_fee_abs", as_type=Decimal)
if fee_abs: if fee_abs:
fees = fees + ["{} {}".format(money_filter(fee_abs, sender.currency), gettext('per order'))] fees = fees + [
"{} {}".format(money_filter(fee_abs, sender.currency), gettext("per order"))
]
fee_percent = sender.settings.get('service_fee_percent', as_type=Decimal) fee_percent = sender.settings.get("service_fee_percent", as_type=Decimal)
if fee_percent: if fee_percent:
fees = fees + ['{} % {}'.format(fee_percent, gettext('per order'))] fees = fees + ["{} % {}".format(fee_percent, gettext("per order"))]
if fee_per_ticket or fee_abs or fee_percent: if fee_per_ticket or fee_abs or fee_percent:
return '<p>%s</p>' % gettext('A service fee of {} will be added to the order total.').format( return "<p>%s</p>" % gettext(
' {} '.format(gettext('plus')).join(fees) "A service fee of {} will be added to the order total."
) ).format(" {} ".format(gettext("plus")).join(fees))
@receiver(order_meta_from_request, dispatch_uid="servicefees_order_meta") @receiver(order_meta_from_request, dispatch_uid="servicefees_order_meta")
@ -217,11 +275,11 @@ def order_meta_signal(sender: Event, request: HttpRequest, **kwargs):
else: else:
try: try:
reseller, user = get_reseller_and_user(request) reseller, user = get_reseller_and_user(request)
meta['servicefees_reseller_id'] = reseller.pk meta["servicefees_reseller_id"] = reseller.pk
except ResellerException: except ResellerException:
pass pass
return meta return meta
settings_hierarkey.add_default('service_fee_skip_addons', 'True', bool) settings_hierarkey.add_default("service_fee_skip_addons", "True", bool)
settings_hierarkey.add_default('service_fee_skip_free', 'True', bool) settings_hierarkey.add_default("service_fee_skip_free", "True", bool)

View File

@ -3,6 +3,9 @@ from django.urls import path
from .views import SettingsView from .views import SettingsView
urlpatterns = [ urlpatterns = [
path('control/event/<str:organizer>/<str:event>/settings/servicefees/', path(
SettingsView.as_view(), name='settings'), "control/event/<str:organizer>/<str:event>/settings/servicefees/",
SettingsView.as_view(),
name="settings",
),
] ]

View File

@ -1,83 +1,98 @@
from django import forms from django import forms
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.forms import SettingsForm from pretix.base.forms import SettingsForm
from pretix.base.models import Event from pretix.base.models import Event
from pretix.control.views.event import EventSettingsViewMixin, EventSettingsFormView from pretix.control.views.event import (
from pretix.helpers.money import change_decimal_field EventSettingsFormView, EventSettingsViewMixin,
)
class ServiceFeeSettingsForm(SettingsForm): class ServiceFeeSettingsForm(SettingsForm):
service_fee_abs = forms.DecimalField( service_fee_abs = forms.DecimalField(label=_("Fixed fee per order"), required=False)
label=_('Fixed fee per order'),
required=False
)
service_fee_percent = forms.DecimalField( service_fee_percent = forms.DecimalField(
label=_('Percentual fee per order'), label=_("Percentual fee per order"),
help_text=_('Percentage of the order total. Note that this percentage will currently only ' help_text=_(
'be calculated on the summed price of sold tickets, not on other fees like e.' "Percentage of the order total. Note that this percentage will currently only "
'g. shipping fees, if there are any.'), "be calculated on the summed price of sold tickets, not on other fees like e."
required=False "g. shipping fees, if there are any."
),
required=False,
) )
service_fee_per_ticket = forms.DecimalField( service_fee_per_ticket = forms.DecimalField(
label=_('Fixed fee per ticket'), label=_("Fixed fee per ticket"),
help_text=_('This fee will be added for each ticket sold, except for free items and addons.'), help_text=_(
required=False "This fee will be added for each ticket sold, except for free items and addons."
),
required=False,
) )
service_fee_skip_if_gift_card = forms.BooleanField( service_fee_skip_if_gift_card = forms.BooleanField(
label=_('Do not charge service fee on tickets paid with gift cards'), label=_("Do not charge service fee on tickets paid with gift cards"),
help_text=_('If a gift card is used for the payment, the percentual fees will be applied on the value of the ' help_text=_(
'tickets minus the value of the gift cards. All fixed fees will be dropped if the tickets can ' "If a gift card is used for the payment, the percentual fees will be applied on the value of the "
'be paid with gift cards entirely. This only works if the gift card is redeemd when the order is ' "tickets minus the value of the gift cards. All fixed fees will be dropped if the tickets can "
'submitted, not if it\'s used to pay an unpaid order later.'), "be paid with gift cards entirely. This only works if the gift card is redeemd when the order is "
required=False "submitted, not if it's used to pay an unpaid order later."
),
required=False,
) )
service_fee_skip_addons = forms.BooleanField( service_fee_skip_addons = forms.BooleanField(
label=_('Do not charge per-ticket service fee on add-on products'), label=_("Do not charge per-ticket service fee on add-on products"),
required=False required=False,
) )
service_fee_skip_non_admission = forms.BooleanField( service_fee_skip_non_admission = forms.BooleanField(
label=_('Do not charge per-ticket service fee on non-admission products'), label=_("Do not charge per-ticket service fee on non-admission products"),
required=False required=False,
) )
service_fee_skip_free = forms.BooleanField( service_fee_skip_free = forms.BooleanField(
label=_('Do not charge per-ticket service fee on free products'), label=_("Do not charge per-ticket service fee on free products"),
help_text=_('Note that regardless of this setting, a per-ticket fee will not be charged if the entire order is free.'), help_text=_(
required=False "Note that regardless of this setting, a per-ticket fee will not be charged if the entire order is free."
),
required=False,
) )
service_fee_split_taxes = forms.BooleanField( service_fee_split_taxes = forms.BooleanField(
label=_('Split taxes proportionate to the tax rates and net values of the ordered products.'), label=_(
help_text=_('If not split based on ordered products, the tax rate falls back to the events base tax rate or no tax, if none is given.'), "Split taxes proportionate to the tax rates and net values of the ordered products."
required=False ),
help_text=_(
"If not split based on ordered products, the tax rate falls back to the events base tax rate or no tax, if none is given."
),
required=False,
) )
service_fee_abs_resellers = forms.DecimalField( service_fee_abs_resellers = forms.DecimalField(
label=_('Fixed fee per order'), label=_("Fixed fee per order"), required=False
required=False
) )
service_fee_percent_resellers = forms.DecimalField( service_fee_percent_resellers = forms.DecimalField(
label=_('Percentual fee per order'), label=_("Percentual fee per order"),
help_text=_('Percentage of the order total. Note that this percentage will currently only ' help_text=_(
'be calculated on the summed price of sold tickets, not on other fees like e.' "Percentage of the order total. Note that this percentage will currently only "
'g. shipping fees, if there are any.'), "be calculated on the summed price of sold tickets, not on other fees like e."
required=False "g. shipping fees, if there are any."
),
required=False,
) )
service_fee_per_ticket_resellers = forms.DecimalField( service_fee_per_ticket_resellers = forms.DecimalField(
label=_('Fixed fee per ticket'), label=_("Fixed fee per ticket"),
required=False, required=False,
help_text=_('This fee will be added for each ticket sold, except for free items and addons.') help_text=_(
"This fee will be added for each ticket sold, except for free items and addons."
),
) )
class SettingsView(EventSettingsViewMixin, EventSettingsFormView): class SettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event model = Event
form_class = ServiceFeeSettingsForm form_class = ServiceFeeSettingsForm
template_name = 'pretix_servicefees/settings.html' template_name = "pretix_servicefees/settings.html"
permission = 'can_change_event_settings' permission = "can_change_event_settings"
def get_success_url(self) -> str: def get_success_url(self) -> str:
return reverse('plugins:pretix_servicefees:settings', kwargs={ return reverse(
'organizer': self.request.event.organizer.slug, "plugins:pretix_servicefees:settings",
'event': self.request.event.slug kwargs={
}) "organizer": self.request.event.organizer.slug,
"event": self.request.event.slug,
},
)