diff --git a/pretix_servicefees/apps.py b/pretix_servicefees/apps.py index 6dbf801..a7b3c99 100644 --- a/pretix_servicefees/apps.py +++ b/pretix_servicefees/apps.py @@ -1,22 +1,23 @@ from django.apps import AppConfig from django.utils.translation import gettext_lazy + from . import __version__ class PluginApp(AppConfig): - name = 'pretix_servicefees' - verbose_name = 'Service Fees' + name = "pretix_servicefees" + verbose_name = "Service Fees" class PretixPluginMeta: - name = gettext_lazy('Service Fees') - author = 'Raphael Michel' - category = 'FEATURE' - description = gettext_lazy('This plugin allows to charge a service fee on all non-free orders.') + name = gettext_lazy("Service Fees") + author = "Raphael Michel" + category = "FEATURE" + description = gettext_lazy( + "This plugin allows to charge a service fee on all non-free orders." + ) visible = True version = __version__ compatibility = "pretix>=4.16.0" def ready(self): from . import signals # NOQA - - diff --git a/pretix_servicefees/signals.py b/pretix_servicefees/signals.py index 031471b..3d60d8d 100644 --- a/pretix_servicefees/signals.py +++ b/pretix_servicefees/signals.py @@ -18,64 +18,83 @@ from pretix.presale.views import get_cart 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): url = resolve(request.path_info) - 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'), - }] + 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): +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: 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: - 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: 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: 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: - 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: - 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: - 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_abs = Decimal("0") if fee_abs is None else fee_abs 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: for p in payment_requests: - if p['provider'] == 'giftcard': - total = max(0, total - Decimal(p['max_value'] or '0')) + if p["provider"] == "giftcard": + total = max(0, total - Decimal(p["max_value"] or "0")) else: # pretix pre 4.15 gift_cards = gift_cards or [] if request: cs = cart_session(request) - if cs.get('gift_cards'): - gift_cards = event.organizer.accepted_gift_cards.filter(pk__in=cs.get('gift_cards'), currency=event.currency) + if cs.get("gift_cards"): + gift_cards = event.organizer.accepted_gift_cards.filter( + pk__in=cs.get("gift_cards"), currency=event.currency + ) summed = 0 for gc in gift_cards: 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. if payment_requests is not None: for p in payment_requests: - 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')) + 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")) - 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() - 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) + 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) if split_taxes: # split taxes based on products ordered - d = defaultdict(lambda: Decimal('0.00')) + d = defaultdict(lambda: Decimal("0.00")) for p in positions: if isinstance(p, CartPosition): 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) sum_base = sum(t[1] for t in base_values) if sum_base: - fee_values = [(t[0], round_decimal(fee * t[1] / sum_base, event.currency)) - for t in base_values] + fee_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) # If there are rounding differences, we fix them up, but always leaning to the benefit of the tax # authorities 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: - 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: 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 = [] for tax_rule, price in fee_values: tax_rule = tax_rule or tax_rule_zero - 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, - tax_value=tax.tax, - tax_rule=tax_rule - )) + 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, + tax_value=tax.tax, + tax_rule=tax_rule, + ) + ) return fees 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") def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwargs): - mod = '' + mod = "" try: from pretix_resellers.utils import ( ResellerException, get_reseller_and_user, @@ -161,48 +197,70 @@ def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwar except ResellerException: pass else: - mod = '_resellers' - return get_fees(sender, total, invoice_address, mod, request, payment_requests=kwargs.get('payment_requests')) + mod = "_resellers" + 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") -def order_fee(sender: Event, positions, invoice_address, total, meta_info, gift_cards, **kwargs): - mod = '' - if meta_info.get('servicefees_reseller_id'): - mod = '_resellers' +def order_fee( + sender: Event, positions, invoice_address, total, meta_info, gift_cards, **kwargs +): + mod = "" + if meta_info.get("servicefees_reseller_id"): + mod = "_resellers" try: from pretix_resellers.models import Reseller except ImportError: pass 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) if config.skip_default_service_fees: return [] - return get_fees(sender, total, invoice_address, mod, positions=positions, gift_cards=gift_cards, - payment_requests=kwargs.get('payment_requests')) + return get_fees( + 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") def front_page_top_recv(sender: Event, **kwargs): 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: - 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: - 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: - 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: - return '

%s

' % gettext('A service fee of {} will be added to the order total.').format( - ' {} '.format(gettext('plus')).join(fees) - ) + return "

%s

" % gettext( + "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") @@ -217,11 +275,11 @@ def order_meta_signal(sender: Event, request: HttpRequest, **kwargs): else: try: reseller, user = get_reseller_and_user(request) - meta['servicefees_reseller_id'] = reseller.pk + meta["servicefees_reseller_id"] = reseller.pk except ResellerException: pass return meta -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_addons", "True", bool) +settings_hierarkey.add_default("service_fee_skip_free", "True", bool) diff --git a/pretix_servicefees/urls.py b/pretix_servicefees/urls.py index fae3b55..4a98794 100644 --- a/pretix_servicefees/urls.py +++ b/pretix_servicefees/urls.py @@ -3,6 +3,9 @@ from django.urls import path from .views import SettingsView urlpatterns = [ - path('control/event///settings/servicefees/', - SettingsView.as_view(), name='settings'), + path( + "control/event///settings/servicefees/", + SettingsView.as_view(), + name="settings", + ), ] diff --git a/pretix_servicefees/views.py b/pretix_servicefees/views.py index 41f67fb..ed15957 100644 --- a/pretix_servicefees/views.py +++ b/pretix_servicefees/views.py @@ -1,83 +1,98 @@ from django import forms from django.urls import reverse from django.utils.translation import gettext_lazy as _ - from pretix.base.forms import SettingsForm from pretix.base.models import Event -from pretix.control.views.event import EventSettingsViewMixin, EventSettingsFormView -from pretix.helpers.money import change_decimal_field +from pretix.control.views.event import ( + EventSettingsFormView, EventSettingsViewMixin, +) class ServiceFeeSettingsForm(SettingsForm): - service_fee_abs = forms.DecimalField( - label=_('Fixed fee per order'), - required=False - ) + service_fee_abs = forms.DecimalField(label=_("Fixed fee per order"), required=False) service_fee_percent = forms.DecimalField( - label=_('Percentual fee per order'), - help_text=_('Percentage of the order total. Note that this percentage will currently only ' - 'be calculated on the summed price of sold tickets, not on other fees like e.' - 'g. shipping fees, if there are any.'), - required=False + label=_("Percentual fee per order"), + help_text=_( + "Percentage of the order total. Note that this percentage will currently only " + "be calculated on the summed price of sold tickets, not on other fees like e." + "g. shipping fees, if there are any." + ), + required=False, ) service_fee_per_ticket = forms.DecimalField( - label=_('Fixed fee per ticket'), - help_text=_('This fee will be added for each ticket sold, except for free items and addons.'), - required=False + label=_("Fixed fee per ticket"), + help_text=_( + "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( - 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 ' - 'tickets minus the value of the gift cards. All fixed fees will be dropped if the tickets can ' - 'be paid with gift cards entirely. This only works if the gift card is redeemd when the order is ' - 'submitted, not if it\'s used to pay an unpaid order later.'), - required=False + 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 " + "tickets minus the value of the gift cards. All fixed fees will be dropped if the tickets can " + "be paid with gift cards entirely. This only works if the gift card is redeemd when the order is " + "submitted, not if it's used to pay an unpaid order later." + ), + required=False, ) service_fee_skip_addons = forms.BooleanField( - label=_('Do not charge per-ticket service fee on add-on products'), - required=False + label=_("Do not charge per-ticket service fee on add-on products"), + required=False, ) service_fee_skip_non_admission = forms.BooleanField( - label=_('Do not charge per-ticket service fee on non-admission products'), - required=False + label=_("Do not charge per-ticket service fee on non-admission products"), + required=False, ) service_fee_skip_free = forms.BooleanField( - 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.'), - required=False + 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." + ), + required=False, ) service_fee_split_taxes = forms.BooleanField( - label=_('Split taxes proportionate to the tax rates and net values of the ordered products.'), - help_text=_('If not split based on ordered products, the tax rate falls back to the event’s base tax rate or no tax, if none is given.'), - required=False + label=_( + "Split taxes proportionate to the tax rates and net values of the ordered products." + ), + help_text=_( + "If not split based on ordered products, the tax rate falls back to the event’s base tax rate or no tax, if none is given." + ), + required=False, ) service_fee_abs_resellers = forms.DecimalField( - label=_('Fixed fee per order'), - required=False + label=_("Fixed fee per order"), required=False ) service_fee_percent_resellers = forms.DecimalField( - label=_('Percentual fee per order'), - help_text=_('Percentage of the order total. Note that this percentage will currently only ' - 'be calculated on the summed price of sold tickets, not on other fees like e.' - 'g. shipping fees, if there are any.'), - required=False + label=_("Percentual fee per order"), + help_text=_( + "Percentage of the order total. Note that this percentage will currently only " + "be calculated on the summed price of sold tickets, not on other fees like e." + "g. shipping fees, if there are any." + ), + required=False, ) service_fee_per_ticket_resellers = forms.DecimalField( - label=_('Fixed fee per ticket'), + label=_("Fixed fee per ticket"), 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): model = Event form_class = ServiceFeeSettingsForm - template_name = 'pretix_servicefees/settings.html' - permission = 'can_change_event_settings' + template_name = "pretix_servicefees/settings.html" + permission = "can_change_event_settings" def get_success_url(self) -> str: - return reverse('plugins:pretix_servicefees:settings', kwargs={ - 'organizer': self.request.event.organizer.slug, - 'event': self.request.event.slug - }) + return reverse( + "plugins:pretix_servicefees:settings", + kwargs={ + "organizer": self.request.event.organizer.slug, + "event": self.request.event.slug, + }, + )