Added a payment provider for free products

This commit is contained in:
Raphael Michel
2015-06-23 10:02:47 +02:00
parent 7308405da5
commit bef9e05e0b
7 changed files with 108 additions and 11 deletions

View File

@@ -66,6 +66,8 @@ The provider class
.. automethod:: checkout_form .. automethod:: checkout_form
.. automethod:: is_allowed
.. autoattribute:: checkout_form_fields .. autoattribute:: checkout_form_fields
.. automethod:: checkout_prepare .. automethod:: checkout_prepare

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-06-21 19:18+0000\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 <michel@rami.io>\n" "Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: Raphael Michel <michel@rami.io>\n" "Language-Team: Raphael Michel <michel@rami.io>\n"
"Language: de\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:4
#: pretix/control/templates/pretixcontrol/order/refund.html:8 #: pretix/control/templates/pretixcontrol/order/refund.html:8
msgid "Refund order" msgid "Refund order"
msgstr "Bestellung erstatteten" msgstr "Bestellung erstatten"
#: pretix/control/templates/pretixcontrol/order/index.html:42 #: pretix/control/templates/pretixcontrol/order/index.html:42
#: pretix/presale/templates/pretixpresale/event/order.html:65 #: pretix/presale/templates/pretixpresale/event/order.html:65

View File

@@ -2,15 +2,19 @@ from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.db.models import Sum, Q
from django.dispatch import receiver
from django.forms import Form from django.forms import Form
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import SettingsForm 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.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
class BasePaymentProvider: class BasePaymentProvider:
@@ -154,6 +158,16 @@ class BasePaymentProvider:
form.fields = self.checkout_form_fields form.fields = self.checkout_form_fields
return form 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: def checkout_form_render(self, request: HttpRequest) -> str:
""" """
When the user selects this provider as his prefered payment method, 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 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. 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 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 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, something inside ``order.payment_info``, please do it after the ``mark_order_paid`` call,
@@ -345,3 +359,67 @@ class BasePaymentProvider:
order.mark_refunded() order.mark_refunded()
messages.success(request, _('The order has been marked as refunded. Please transfer the money ' messages.success(request, _('The order has been marked as refunded. Please transfer the money '
'back to the buyer manually.')) '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

View File

@@ -29,6 +29,7 @@ class EventPluginSignal(django.dispatch.Signal):
# Find the Django application this belongs to # Find the Django application this belongs to
searchpath = receiver.__module__ searchpath = receiver.__module__
app = None app = None
mod = None
while "." in searchpath: while "." in searchpath:
try: try:
if apps.is_installed(searchpath): if apps.is_installed(searchpath):
@@ -37,8 +38,8 @@ class EventPluginSignal(django.dispatch.Signal):
pass pass
searchpath, mod = searchpath.rsplit(".", 1) searchpath, mod = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins # Only fire receivers from active plugins and core modules
if app.name in sender.get_plugins(): 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: if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
response = receiver(signal=self, sender=sender, **named) response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response)) responses.append((receiver, response))

View File

@@ -136,7 +136,9 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
) )
provider.settings_content = provider.settings_content_render(self.request) provider.settings_content = provider.settings_content_render(self.request)
provider.form.prepare_fields() 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 return providers
def get_context_data(self, *args, **kwargs) -> dict: def get_context_data(self, *args, **kwargs) -> dict:

View File

@@ -35,16 +35,17 @@
</div> </div>
</div> </div>
</div> </div>
{# TODO: Question answers #}
<div class="row-fluid"> <div class="row-fluid">
<div class="panel panel-primary"> <div class="panel panel-primary">
<div class="panel-heading"> <div class="panel-heading">
{% if payment_provider.identifier != "free" %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url "presale:event.checkout.payment" organizer=request.event.organizer.slug event=request.event.slug %}"> <a href="{% url "presale:event.checkout.payment" organizer=request.event.organizer.slug event=request.event.slug %}">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
{% trans "Modify" %} {% trans "Modify" %}
</a> </a>
</div> </div>
{% endif %}
<h3 class="panel-title"> <h3 class="panel-title">
{% trans "Payment" %} {% trans "Payment" %}
</h3> </h3>
@@ -57,7 +58,7 @@
<div class="row checkout-button-row"> <div class="row checkout-button-row">
<div class="col-md-4"> <div class="col-md-4">
<a class="btn btn-block btn-default btn-lg" <a class="btn btn-block btn-default btn-lg"
href="{{ view.get_payment_url }}"> href="{{ view.get_previous_url }}">
{% trans "Go back" %} {% trans "Go back" %}
</a> </a>
</div> </div>

View File

@@ -153,7 +153,7 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin,
responses = register_payment_providers.send(self.request.event) responses = register_payment_providers.send(self.request.event)
for receiver, response in responses: for receiver, response in responses:
provider = response(self.request.event) provider = response(self.request.event)
if not provider.is_enabled: if not provider.is_enabled or not provider.is_allowed(self.request):
continue continue
fee = provider.calculate_fee(self._total_order_value) fee = provider.calculate_fee(self._total_order_value)
providers.append({ providers.append({
@@ -178,6 +178,12 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin,
messages.error(self.request, _("Please select a payment method.")) messages.error(self.request, _("Please select a payment method."))
return self.get(request, *args, **kwargs) 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['providers'] = self.provider_forms ctx['providers'] = self.provider_forms
@@ -208,6 +214,7 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['cart'] = self.get_cart(answers=True) ctx['cart'] = self.get_cart(answers=True)
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request) ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
ctx['payment_provider'] = self.payment_provider
return ctx return ctx
@cached_property @cached_property
@@ -225,7 +232,9 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch
if 'payment' not in request.session or not self.payment_provider: if 'payment' not in request.session or not self.payment_provider:
messages.error(request, _('The payment information you entered was incomplete.')) messages.error(request, _('The payment information you entered was incomplete.'))
return redirect(self.get_payment_url()) 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.')) messages.error(request, _('The payment information you entered was incomplete.'))
return redirect(self.get_payment_url()) return redirect(self.get_payment_url())
for cp in self.positions: for cp in self.positions:
@@ -348,3 +357,7 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch
) )
OrderPosition.transform_cart_positions(cartpos, order) OrderPosition.transform_cart_positions(cartpos, order)
return order return order
def get_previous_url(self):
if self.payment_provider != "free":
return self.get_payment_url()