forked from CGM_Public/pretix_original
Fix #177 - Allow to change the payment method
This commit is contained in:
@@ -68,6 +68,8 @@ The provider class
|
|||||||
|
|
||||||
.. automethod:: is_allowed
|
.. automethod:: is_allowed
|
||||||
|
|
||||||
|
.. automethod:: is_allowed_for_order
|
||||||
|
|
||||||
.. autoattribute:: payment_form_fields
|
.. autoattribute:: payment_form_fields
|
||||||
|
|
||||||
.. automethod:: checkout_prepare
|
.. automethod:: checkout_prepare
|
||||||
@@ -86,9 +88,11 @@ The provider class
|
|||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
|
.. automethod:: order_change_allowed
|
||||||
|
|
||||||
.. automethod:: order_can_retry
|
.. automethod:: order_can_retry
|
||||||
|
|
||||||
.. automethod:: retry_prepare
|
.. automethod:: order_prepare
|
||||||
|
|
||||||
.. automethod:: order_paid_render
|
.. automethod:: order_paid_render
|
||||||
|
|
||||||
|
|||||||
@@ -314,20 +314,42 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
|
def order_change_allowed(self, order: Order) -> bool:
|
||||||
|
"""
|
||||||
|
Will be called to check whether it is allowed to change the payment method of
|
||||||
|
an order to this one.
|
||||||
|
|
||||||
|
The default implementation always returns ``True``.
|
||||||
|
|
||||||
|
:param order: The order object
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
def order_can_retry(self, order: Order) -> bool:
|
def order_can_retry(self, order: Order) -> bool:
|
||||||
"""
|
"""
|
||||||
Will be called if the user views the detail page of an unpaid order to determine
|
Will be called if the user views the detail page of an unpaid order to determine
|
||||||
whether the user should be presented with an option to retry the payment. The default
|
whether the user should be presented with an option to retry the payment. The default
|
||||||
implementation always returns False.
|
implementation always returns False.
|
||||||
|
|
||||||
|
The retry workflow is also used if a user switches to this payment method for an existing
|
||||||
|
order! Therefore, they can only switch to your p
|
||||||
|
|
||||||
:param order: The order object
|
:param order: The order object
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||||
|
"""
|
||||||
|
Deprecated, use order_prepare instead
|
||||||
|
"""
|
||||||
|
raise DeprecationWarning('retry_prepare is deprecated, use order_prepare instead')
|
||||||
|
return self.order_prepare(request, order)
|
||||||
|
|
||||||
|
def order_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||||
"""
|
"""
|
||||||
Will be called if the user retries to pay an unpaid order (after the user filled in
|
Will be called if the user retries to pay an unpaid order (after the user filled in
|
||||||
e.g. the form returned by :py:meth:`payment_form`).
|
e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
|
||||||
|
method.
|
||||||
|
|
||||||
It should return and report errors the same way as :py:meth:`checkout_prepare`, but
|
It should return and report errors the same way as :py:meth:`checkout_prepare`, but
|
||||||
receives an ``Order`` object instead of a cart object.
|
receives an ``Order`` object instead of a cart object.
|
||||||
@@ -469,6 +491,9 @@ class FreeOrderProvider(BasePaymentProvider):
|
|||||||
cart_id=request.session.session_key, event=request.event
|
cart_id=request.session.session_key, event=request.event
|
||||||
).aggregate(sum=Sum('price'))['sum'] == 0
|
).aggregate(sum=Sum('price'))['sum'] == 0
|
||||||
|
|
||||||
|
def order_change_allowed(self, order: Order) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
||||||
def register_payment_provider(sender, **kwargs):
|
def register_payment_provider(sender, **kwargs):
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class Paypal(BasePaymentProvider):
|
|||||||
def order_can_retry(self, order):
|
def order_can_retry(self, order):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def retry_prepare(self, request, order):
|
def order_prepare(self, request, order):
|
||||||
self.init_api()
|
self.init_api()
|
||||||
payment = paypalrestsdk.Payment({
|
payment = paypalrestsdk.Payment({
|
||||||
'intent': 'sale',
|
'intent': 'sale',
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class Stripe(BasePaymentProvider):
|
|||||||
def payment_is_valid_session(self, request):
|
def payment_is_valid_session(self, request):
|
||||||
return request.session.get('payment_stripe_token') != ''
|
return request.session.get('payment_stripe_token') != ''
|
||||||
|
|
||||||
def retry_prepare(self, request, order):
|
def order_prepare(self, request, order):
|
||||||
return self.checkout_prepare(request, None)
|
return self.checkout_prepare(request, None)
|
||||||
|
|
||||||
def checkout_prepare(self, request, cart):
|
def checkout_prepare(self, request, cart):
|
||||||
|
|||||||
@@ -33,6 +33,12 @@
|
|||||||
{% if order.status == "n" %}
|
{% if order.status == "n" %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="{% eventurl event "presale:event.order.pay.change" secret=order.secret order=order.code %}">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
{% trans "Use different payment method" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
{% trans "Payment" %}
|
{% trans "Payment" %}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{% extends "pretixpresale/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load eventurl %}
|
||||||
|
{% block title %}{% trans "Change payment method" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>
|
||||||
|
{% blocktrans trimmed with code=order.code %}
|
||||||
|
Change payment method: {{ code }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please note: If you change your payment method, your order total will change by the
|
||||||
|
amount displayed to the right of each method.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="panel-group" id="payment_accordion">
|
||||||
|
{% for p in providers %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<label class="radio">
|
||||||
|
<strong class="pull-right">{% if p.fee_diff >= 0 %}+{% else %}-{% endif %} {{ p.fee_diff_abs|floatformat:2 }} {{ event.currency }}</strong>
|
||||||
|
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||||
|
data-parent="#payment_accordion"
|
||||||
|
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||||
|
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}" />
|
||||||
|
<strong>{{ p.provider.verbose_name }}</strong>
|
||||||
|
</label>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="payment_{{ p.provider.identifier }}"
|
||||||
|
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
|
||||||
|
<div class="panel-body form-horizontal">
|
||||||
|
{{ p.form }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% trans "There are no alternative payment providers available for this order." %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="row checkout-button-row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a class="btn btn-block btn-default btn-lg"
|
||||||
|
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
{% if providers %}
|
||||||
|
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||||
|
{% trans "Continue" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -40,6 +40,9 @@ event_patterns = [
|
|||||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/complete$',
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/complete$',
|
||||||
pretix.presale.views.order.OrderPayComplete.as_view(),
|
pretix.presale.views.order.OrderPayComplete.as_view(),
|
||||||
name='event.order.pay.complete'),
|
name='event.order.pay.complete'),
|
||||||
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
|
||||||
|
pretix.presale.views.order.OrderPayChangeMethod.as_view(),
|
||||||
|
name='event.order.pay.change'),
|
||||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
|
||||||
pretix.presale.views.order.OrderDownload.as_view(),
|
pretix.presale.views.order.OrderDownload.as_view(),
|
||||||
name='event.order.download'),
|
name='event.order.download'),
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Sum
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -14,7 +16,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.orders import InvoiceAddress
|
from pretix.base.models.orders import InvoiceAddress
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_invoice, invoice_pdf, invoice_qualified,
|
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||||
)
|
)
|
||||||
from pretix.base.services.orders import OrderError, cancel_order
|
from pretix.base.services.orders import OrderError, cancel_order
|
||||||
from pretix.base.services.tickets import generate
|
from pretix.base.services.tickets import generate
|
||||||
@@ -125,9 +127,9 @@ class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
resp = self.payment_provider.retry_prepare(
|
resp = self.payment_provider.order_prepare(request, self.order)
|
||||||
request, self.order
|
if 'payment_change_{}'.format(self.order.pk) in request.session:
|
||||||
)
|
del request.session['payment_change_{}'.format(self.order.pk)]
|
||||||
if isinstance(resp, str):
|
if isinstance(resp, str):
|
||||||
return redirect(resp)
|
return redirect(resp)
|
||||||
elif resp is True:
|
elif resp is True:
|
||||||
@@ -181,7 +183,8 @@ class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView):
|
|||||||
self.request = request
|
self.request = request
|
||||||
if not self.order:
|
if not self.order:
|
||||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||||
if not self.payment_provider.order_can_retry(self.order) or not self.payment_provider.is_enabled:
|
can_do = self.payment_provider.order_can_retry(self.order) or 'payment_change_{}'.format(self.order.pk) in request.session
|
||||||
|
if not can_do or not self.payment_provider.is_enabled:
|
||||||
messages.error(request, _('The payment for this order cannot be continued.'))
|
messages.error(request, _('The payment for this order cannot be continued.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
if (not self.payment_provider.payment_is_valid_session(request)
|
if (not self.payment_provider.payment_is_valid_session(request)
|
||||||
@@ -192,6 +195,8 @@ class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
resp = self.payment_provider.payment_perform(request, self.order)
|
resp = self.payment_provider.payment_perform(request, self.order)
|
||||||
|
if 'payment_change_{}'.format(self.order.pk) in request.session:
|
||||||
|
del request.session['payment_change_{}'.format(self.order.pk)]
|
||||||
return redirect(resp or self.get_order_url())
|
return redirect(resp or self.get_order_url())
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@@ -213,8 +218,8 @@ class OrderPayComplete(EventViewMixin, OrderDetailMixin, View):
|
|||||||
self.request = request
|
self.request = request
|
||||||
if not self.order:
|
if not self.order:
|
||||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||||
if (not self.payment_provider.payment_is_valid_session(request)
|
if (not self.payment_provider.payment_is_valid_session(request) or
|
||||||
or not self.payment_provider.is_enabled):
|
not self.payment_provider.is_enabled):
|
||||||
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())
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@@ -230,6 +235,96 @@ class OrderPayComplete(EventViewMixin, OrderDetailMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||||
|
template_name = 'pretixpresale/event/order_pay_change.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
if not self.order:
|
||||||
|
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||||
|
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||||
|
messages.error(request, _('The payment method for this order cannot be changed.'))
|
||||||
|
return redirect(self.get_order_url() + '?paid=yes')
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_payment_url(self):
|
||||||
|
return eventreverse(self.request.event, 'presale:event.order.pay', kwargs={
|
||||||
|
'order': self.order.code,
|
||||||
|
'secret': self.order.secret
|
||||||
|
})
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _total_order_value(self):
|
||||||
|
return self.order.positions.aggregate(sum=Sum('price'))['sum']
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def provider_forms(self):
|
||||||
|
providers = []
|
||||||
|
responses = register_payment_providers.send(self.request.event)
|
||||||
|
for receiver, response in responses:
|
||||||
|
provider = response(self.request.event)
|
||||||
|
if provider.identifier == self.order.payment_provider:
|
||||||
|
continue
|
||||||
|
if not provider.is_enabled or not provider.order_change_allowed(self.order):
|
||||||
|
continue
|
||||||
|
fee = provider.calculate_fee(self._total_order_value)
|
||||||
|
providers.append({
|
||||||
|
'provider': provider,
|
||||||
|
'fee': fee,
|
||||||
|
'fee_diff': fee - self.order.payment_fee,
|
||||||
|
'fee_diff_abs': abs(fee - self.order.payment_fee),
|
||||||
|
'form': provider.payment_form_render(self.request)
|
||||||
|
})
|
||||||
|
return providers
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
for p in self.provider_forms:
|
||||||
|
if p['provider'].identifier == request.POST.get('payment', ''):
|
||||||
|
request.session['payment'] = p['provider'].identifier
|
||||||
|
request.session['payment_change_{}'.format(self.order.pk)] = '1'
|
||||||
|
|
||||||
|
resp = p['provider'].order_prepare(request, self.order)
|
||||||
|
if resp:
|
||||||
|
with transaction.atomic():
|
||||||
|
new_fee = p['provider'].calculate_fee(self._total_order_value)
|
||||||
|
self.order.log_action('pretix.event.order.payment.changed', {
|
||||||
|
'old_fee': self.order.payment_fee,
|
||||||
|
'new_fee': new_fee,
|
||||||
|
'old_provider': self.order.payment_provider,
|
||||||
|
'new_provider': p['provider'].identifier
|
||||||
|
})
|
||||||
|
self.order.payment_provider = p['provider'].identifier
|
||||||
|
self.order.payment_fee = new_fee
|
||||||
|
self.order._calculate_tax()
|
||||||
|
self.order.save()
|
||||||
|
|
||||||
|
i = self.order.invoices.filter(is_cancellation=False).last()
|
||||||
|
if i:
|
||||||
|
generate_cancellation(i)
|
||||||
|
generate_invoice(self.order)
|
||||||
|
if isinstance(resp, str):
|
||||||
|
return redirect(resp)
|
||||||
|
elif resp is True:
|
||||||
|
return redirect(self.get_confirm_url())
|
||||||
|
else:
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
messages.error(self.request, _("Please select a payment method."))
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['order'] = self.order
|
||||||
|
ctx['providers'] = self.provider_forms
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_confirm_url(self):
|
||||||
|
return eventreverse(self.request.event, 'presale:event.order.pay.confirm', kwargs={
|
||||||
|
'order': self.order.code,
|
||||||
|
'secret': self.order.secret
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
|
class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
|
||||||
template_name = "pretixpresale/event/order_modify.html"
|
template_name = "pretixpresale/event/order_modify.html"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user