diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst index c2fc3b164..681688ddf 100644 --- a/doc/development/api/payment.rst +++ b/doc/development/api/payment.rst @@ -66,6 +66,8 @@ The provider class .. automethod:: checkout_form + .. automethod:: is_allowed + .. autoattribute:: checkout_form_fields .. automethod:: checkout_prepare diff --git a/src/locale/de/LC_MESSAGES/django.po b/src/locale/de/LC_MESSAGES/django.po index e5370c657..f0936bcf5 100644 --- a/src/locale/de/LC_MESSAGES/django.po +++ b/src/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-06-21 19:18+0000\n" -"PO-Revision-Date: 2015-06-21 21:19+0100\n" +"PO-Revision-Date: 2015-06-23 09:51+0100\n" "Last-Translator: Raphael Michel \n" "Language-Team: Raphael Michel \n" "Language: de\n" @@ -1483,7 +1483,7 @@ msgstr "Als nicht bezahlt markieren" #: pretix/control/templates/pretixcontrol/order/refund.html:4 #: pretix/control/templates/pretixcontrol/order/refund.html:8 msgid "Refund order" -msgstr "Bestellung erstatteten" +msgstr "Bestellung erstatten" #: pretix/control/templates/pretixcontrol/order/index.html:42 #: pretix/presale/templates/pretixpresale/event/order.html:65 diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index d26dc61ed..259dec1aa 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -2,15 +2,19 @@ from collections import OrderedDict from decimal import Decimal from django import forms from django.contrib import messages +from django.db.models import Sum, Q +from django.dispatch import receiver from django.forms import Form from django.http import HttpRequest from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ from pretix.base.forms import SettingsForm -from pretix.base.models import Order +from pretix.base.models import Order, CartPosition +from pretix.base.services.orders import mark_order_paid from pretix.base.settings import SettingsSandbox +from pretix.base.signals import register_payment_providers class BasePaymentProvider: @@ -154,6 +158,16 @@ class BasePaymentProvider: form.fields = self.checkout_form_fields return form + def is_allowed(self, request: HttpRequest) -> bool: + """ + You can use this method to disable this payment provider for certain groups + of users, products or other criteria. If this method returns ``False``, the + user will not be able to select this payment method. + + The default implementation always returns ``True``. + """ + return True + def checkout_form_render(self, request: HttpRequest) -> str: """ When the user selects this provider as his prefered payment method, @@ -243,7 +257,7 @@ class BasePaymentProvider: containing an URL the user will be redirected to. If you are done with your process you should return the user to the order's detail page. - If the payment is completed, you should call ``pretix.bsae.services.orders.mark_order_paid(order, provider, info)`` + If the payment is completed, you should call ``pretix.base.services.orders.mark_order_paid(order, provider, info)`` with ``provider`` being your :py:attr:`identifier` and ``info`` being any string you might want to store for later usage. Please note, that if you want to store something inside ``order.payment_info``, please do it after the ``mark_order_paid`` call, @@ -345,3 +359,67 @@ class BasePaymentProvider: order.mark_refunded() messages.success(request, _('The order has been marked as refunded. Please transfer the money ' 'back to the buyer manually.')) + + +class FreeOrderProvider(BasePaymentProvider): + + @property + def is_enabled(self) -> bool: + return True + + @property + def identifier(self) -> str: + return "free" + + def checkout_confirm_render(self, request) -> str: + return _("No payment is required as this order only includes products which are free of charge.") + + def order_pending_render(self, request: HttpRequest, order: Order) -> str: + pass + + def checkout_is_valid_session(self, request: HttpRequest) -> bool: + return True + + @property + def verbose_name(self) -> str: + return _("Free of charge") + + def checkout_perform(self, request: HttpRequest, order: Order): + mark_order_paid(order, 'free') + + @property + def settings_form_fields(self) -> dict: + return {} + + def order_control_refund_render(self, order: Order) -> str: + return '' + + def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str": + """ + Will be called if the event administrator confirms the refund. + + This should transfer the money back (if possible). You can return an URL the + user should be redirected to if you need special behaviour or None to continue + with default behaviour. + + On failure, you should use Django's message framework to display an error message + to the user. + + The default implementation sets the Orders state to refunded and shows a success + message. + + :param request: The HTTP request + :param order: The order object + """ + order.mark_refunded() + messages.success(request, _('The order has been marked as refunded.')) + + def is_allowed(self, request: HttpRequest) -> bool: + return CartPosition.objects.current.filter( + Q(user=request.user) & Q(event=request.event) + ).aggregate(sum=Sum('price'))['sum'] == 0 + + +@receiver(register_payment_providers) +def register_payment_provider(sender, **kwargs): + return FreeOrderProvider diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index 8d26d0c61..92ffe3ee7 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -29,6 +29,7 @@ class EventPluginSignal(django.dispatch.Signal): # Find the Django application this belongs to searchpath = receiver.__module__ app = None + mod = None while "." in searchpath: try: if apps.is_installed(searchpath): @@ -37,8 +38,8 @@ class EventPluginSignal(django.dispatch.Signal): pass searchpath, mod = searchpath.rsplit(".", 1) - # Only fire receivers from active plugins - if app.name in sender.get_plugins(): + # Only fire receivers from active plugins and core modules + if (searchpath, mod) == ("pretix", "base") or (app and app.name in sender.get_plugins()): if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors: response = receiver(signal=self, sender=sender, **named) responses.append((receiver, response)) diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 571d6bd98..b2713f134 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -136,7 +136,9 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi ) provider.settings_content = provider.settings_content_render(self.request) provider.form.prepare_fields() - providers.append(provider) + if provider.settings_content or provider.form.fields: + # Exclude providers which do not provide any settings + providers.append(provider) return providers def get_context_data(self, *args, **kwargs) -> dict: diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html index 632c67711..bea9fd9e5 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html @@ -35,16 +35,17 @@ - {# TODO: Question answers #}
+ {% if payment_provider.identifier != "free" %} + {% endif %}

{% trans "Payment" %}

@@ -57,7 +58,7 @@
diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index 837993374..8adb6d200 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -153,7 +153,7 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, responses = register_payment_providers.send(self.request.event) for receiver, response in responses: provider = response(self.request.event) - if not provider.is_enabled: + if not provider.is_enabled or not provider.is_allowed(self.request): continue fee = provider.calculate_fee(self._total_order_value) providers.append({ @@ -178,6 +178,12 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, messages.error(self.request, _("Please select a payment method.")) return self.get(request, *args, **kwargs) + def get(self, request, *args, **kwargs): + if self._total_order_value == 0: + request.session['payment'] = 'free' + return redirect(self.get_confirm_url()) + return super().get(request, *args, **kwargs) + def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['providers'] = self.provider_forms @@ -208,6 +214,7 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch ctx = super().get_context_data(**kwargs) ctx['cart'] = self.get_cart(answers=True) ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request) + ctx['payment_provider'] = self.payment_provider return ctx @cached_property @@ -225,7 +232,9 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch if 'payment' not in request.session or not self.payment_provider: messages.error(request, _('The payment information you entered was incomplete.')) return redirect(self.get_payment_url()) - if not self.payment_provider.checkout_is_valid_session(request): + if not self.payment_provider.checkout_is_valid_session(request) or \ + not self.payment_provider.is_enabled or \ + not self.payment_provider.is_allowed(request): messages.error(request, _('The payment information you entered was incomplete.')) return redirect(self.get_payment_url()) for cp in self.positions: @@ -348,3 +357,7 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch ) OrderPosition.transform_cart_positions(cartpos, order) return order + + def get_previous_url(self): + if self.payment_provider != "free": + return self.get_payment_url()