mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
Support for external gift cards (#2912)
This commit is contained in:
@@ -33,12 +33,13 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import copy
|
||||
import inspect
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.core.signing import BadSignature, loads
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import F, Q
|
||||
@@ -56,12 +57,14 @@ from pretix.base.models import Customer, Membership, Order
|
||||
from pretix.base.models.orders import InvoiceAddress, OrderPayment
|
||||
from pretix.base.models.tax import TaxedPrice, TaxRule
|
||||
from pretix.base.services.cart import (
|
||||
CartError, CartManager, error_messages, get_fees, set_cart_addons,
|
||||
CartError, CartManager, add_payment_to_cart, error_messages, get_fees,
|
||||
set_cart_addons,
|
||||
)
|
||||
from pretix.base.services.memberships import validate_memberships_in_order
|
||||
from pretix.base.services.orders import perform_order
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import validate_cart_addons
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.base.templatetags.rich_text import rich_text_snippet
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
@@ -1144,53 +1147,153 @@ class PaymentStep(CartMixin, TemplateFlowStep):
|
||||
})
|
||||
return providers
|
||||
|
||||
@cached_property
|
||||
def single_use_payment(self):
|
||||
singleton_payments = [p for p in self.cart_session.get('payments', []) if not p.get('multi_use_supported')]
|
||||
if not singleton_payments:
|
||||
return None
|
||||
return singleton_payments[0]
|
||||
|
||||
def current_payments_valid(self, amount):
|
||||
singleton_payments = [p for p in self.cart_session.get('payments', []) if not p.get('multi_use_supported')]
|
||||
if len(singleton_payments) > 1:
|
||||
return False
|
||||
|
||||
matched = Decimal('0.00')
|
||||
for p in self.cart_session.get('payments', []):
|
||||
if p.get('min_value') and (amount - matched) < Decimal(p['min_value']):
|
||||
continue
|
||||
if p.get('max_value') and (amount - matched) > Decimal(p['max_value']):
|
||||
matched += Decimal(p['max_value'])
|
||||
else:
|
||||
matched = Decimal('0.00')
|
||||
|
||||
return matched == Decimal('0.00'), amount - matched
|
||||
|
||||
def post(self, request):
|
||||
self.request = request
|
||||
|
||||
if "remove_payment" in request.POST:
|
||||
self._remove_payment(request.POST["remove_payment"])
|
||||
return redirect(self.get_step_url(request))
|
||||
|
||||
for p in self.provider_forms:
|
||||
if p['provider'].identifier == request.POST.get('payment', ''):
|
||||
self.cart_session['payment'] = p['provider'].identifier
|
||||
resp = p['provider'].checkout_prepare(
|
||||
pprov = p['provider']
|
||||
if pprov.identifier == request.POST.get('payment', ''):
|
||||
if not pprov.multi_use_supported:
|
||||
# Providers with multi_use_supported will call this themselves
|
||||
simulated_payments = self.cart_session.get('payments', {})
|
||||
simulated_payments = [p for p in simulated_payments if p.get('multi_use_supported')]
|
||||
simulated_payments.append({
|
||||
'provider': pprov.identifier,
|
||||
'multi_use_supported': False,
|
||||
'min_value': None,
|
||||
'max_value': None,
|
||||
'info_data': {},
|
||||
})
|
||||
cart = self.get_cart(payments=simulated_payments)
|
||||
else:
|
||||
cart = self.get_cart()
|
||||
resp = pprov.checkout_prepare(
|
||||
request,
|
||||
self.get_cart()
|
||||
cart,
|
||||
)
|
||||
if isinstance(resp, str):
|
||||
return redirect(resp)
|
||||
elif resp is True:
|
||||
return redirect(self.get_next_url(request))
|
||||
if pprov.multi_use_supported:
|
||||
# Provider needs to call add_payment_to_cart itself, but we need to remove all previously
|
||||
# selected ones that don't have multi_use supported. Otherwise, if you first select a credit
|
||||
# card, then go back and switch to a gift card, you'll have both in the session and the credit
|
||||
# card has preference, which is unexpected.
|
||||
self.cart_session['payments'] = [p for p in self.cart_session.get('payments', []) if p.get('multi_use_supported')]
|
||||
|
||||
if pprov.identifier not in [p['provider'] for p in self.cart_session.get('payments', [])]:
|
||||
raise ImproperlyConfigured(f'Payment provider {pprov.identifier} set multi_use_supported '
|
||||
f'and returned True from payment_prepare, but did not call '
|
||||
f'add_payment_to_cart')
|
||||
|
||||
valid, remainder = self.current_payments_valid(cart['total'])
|
||||
if valid:
|
||||
return redirect(self.get_next_url(request))
|
||||
else:
|
||||
# Show payment step again to select another method
|
||||
messages.success(
|
||||
request,
|
||||
_("Your payment method has been applied, but {} still need to be paid. Please select "
|
||||
"a payment method for the remainder.").format(
|
||||
money_filter(remainder, self.event.currency)
|
||||
)
|
||||
)
|
||||
return redirect(self.get_step_url(request))
|
||||
else:
|
||||
# There can only be one payment method that does not have multi_use_supported, remove all
|
||||
# previous ones.
|
||||
self.cart_session['payments'] = [p for p in self.cart_session.get('payments', []) if p.get('multi_use_supported')]
|
||||
add_payment_to_cart(request, pprov, None, None, None)
|
||||
return redirect(self.get_next_url(request))
|
||||
else:
|
||||
return self.render()
|
||||
|
||||
if self.is_completed(request, warn=False):
|
||||
# All payments already accounted for, no need to select one
|
||||
return redirect(self.get_next_url(request))
|
||||
|
||||
messages.error(self.request, _("Please select a payment method."))
|
||||
return self.render()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['current_payments'] = [p for p in self.current_selected_payments(self._total_order_value) if p.get('multi_use_supported')]
|
||||
ctx['remaining'] = self._total_order_value - sum(p['payment_amount'] for p in ctx['current_payments']) + sum(p['fee'] for p in ctx['current_payments'])
|
||||
ctx['providers'] = self.provider_forms
|
||||
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
|
||||
ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', ''))
|
||||
|
||||
if len(self.provider_forms) == 1:
|
||||
ctx['selected'] = self.provider_forms[0]['provider'].identifier
|
||||
elif 'payment' in self.request.POST:
|
||||
ctx['selected'] = self.request.POST['payment']
|
||||
elif self.single_use_payment:
|
||||
ctx['selected'] = self.single_use_payment['provider']
|
||||
else:
|
||||
ctx['selected'] = ''
|
||||
ctx['cart'] = self.get_cart()
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def payment_provider(self):
|
||||
return self.request.event.get_payment_providers().get(self.cart_session['payment'])
|
||||
|
||||
def _is_allowed(self, prov, request):
|
||||
return prov.is_allowed(request, total=self._total_order_value)
|
||||
|
||||
def is_completed(self, request, warn=False):
|
||||
self.request = request
|
||||
if 'payment' not in self.cart_session or not self.payment_provider:
|
||||
if not self.cart_session.get('payments'):
|
||||
if warn:
|
||||
messages.error(request, _('The payment information you entered was incomplete.'))
|
||||
messages.error(request, _('Please select a payment method to proceed.'))
|
||||
return False
|
||||
if not self.payment_provider.payment_is_valid_session(request) or \
|
||||
not self.payment_provider.is_enabled or \
|
||||
not self._is_allowed(self.payment_provider, request):
|
||||
|
||||
cart = get_cart(self.request)
|
||||
total = get_cart_total(self.request)
|
||||
total += sum([f.value for f in get_fees(self.request.event, self.request, total, self.invoice_address,
|
||||
self.cart_session.get('payments', []), cart)])
|
||||
selected = self.current_selected_payments(total, warn=warn, total_includes_payment_fees=True)
|
||||
if sum(p['payment_amount'] for p in selected) != total:
|
||||
if warn:
|
||||
messages.error(request, _('The payment information you entered was incomplete.'))
|
||||
messages.error(request, _('Please select a payment method to proceed.'))
|
||||
return False
|
||||
|
||||
if len([p for p in selected if not p['multi_use_supported']]) > 1:
|
||||
raise ImproperlyConfigured('Multiple non-multi-use providers in session, should never happen')
|
||||
|
||||
for p in selected:
|
||||
if not p['pprov'] or not p['pprov'].is_enabled or not self._is_allowed(p['pprov'], request):
|
||||
self._remove_payment(p['id'])
|
||||
if p['payment_amount']:
|
||||
if warn:
|
||||
messages.error(request, _('Please select a payment method to proceed.'))
|
||||
return False
|
||||
|
||||
if not p['multi_use_supported'] and not p['pprov'].payment_is_valid_session(request):
|
||||
if warn:
|
||||
messages.error(request, _('The payment information you entered was incomplete.'))
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_applicable(self, request):
|
||||
@@ -1198,18 +1301,28 @@ class PaymentStep(CartMixin, TemplateFlowStep):
|
||||
|
||||
for cartpos in get_cart(self.request):
|
||||
if cartpos.requires_approval(invoice_address=self.invoice_address):
|
||||
if 'payment' in self.cart_session:
|
||||
del self.cart_session['payment']
|
||||
if 'payments' in self.cart_session:
|
||||
del self.cart_session['payments']
|
||||
return False
|
||||
|
||||
used_providers = {p['provider'] for p in self.cart_session.get('payments', [])}
|
||||
for p in self.request.event.get_payment_providers().values():
|
||||
if p.is_implicit(request) if callable(p.is_implicit) else p.is_implicit:
|
||||
if self._is_allowed(p, request):
|
||||
self.cart_session['payment'] = p.identifier
|
||||
self.cart_session['payments'] = [
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'provider': p.identifier,
|
||||
'multi_use_supported': False,
|
||||
'min_value': None,
|
||||
'max_value': None,
|
||||
'info_data': {},
|
||||
}
|
||||
]
|
||||
return False
|
||||
elif self.cart_session.get('payment') == p.identifier:
|
||||
elif p.identifier in used_providers:
|
||||
# is_allowed might have changed, e.g. after add-on selection
|
||||
del self.cart_session['payment']
|
||||
self.cart_session['payments'] = [p for p in self.cart_session['payments'] if p['provider'] != p.identifier]
|
||||
|
||||
return True
|
||||
|
||||
@@ -1239,9 +1352,16 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['cart'] = self.get_cart(answers=True)
|
||||
if self.payment_provider:
|
||||
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
|
||||
ctx['payment_provider'] = self.payment_provider
|
||||
|
||||
selected_payments = self.current_selected_payments(ctx['cart']['total'], total_includes_payment_fees=True)
|
||||
ctx['payments'] = []
|
||||
for p in selected_payments:
|
||||
if 'info_data' in inspect.signature(p['pprov'].checkout_confirm_render).parameters:
|
||||
block = p['pprov'].checkout_confirm_render(self.request, info_data=p['info_data'])
|
||||
else:
|
||||
block = p['pprov'].checkout_confirm_render(self.request)
|
||||
ctx['payments'].append((p, block))
|
||||
|
||||
ctx['require_approval'] = any(cp.requires_approval(invoice_address=self.invoice_address) for cp in ctx['cart']['positions'])
|
||||
ctx['addr'] = self.invoice_address
|
||||
ctx['confirm_messages'] = self.confirm_messages
|
||||
@@ -1326,23 +1446,27 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
|
||||
return self.do(
|
||||
self.request.event.id,
|
||||
payment_provider=self.payment_provider.identifier if self.payment_provider else None,
|
||||
payments=self.cart_session.get('payments', []),
|
||||
positions=[p.id for p in self.positions],
|
||||
email=self.cart_session.get('email'),
|
||||
locale=translation.get_language(),
|
||||
address=self.invoice_address.pk,
|
||||
meta_info=meta_info,
|
||||
sales_channel=request.sales_channel.identifier,
|
||||
gift_cards=self.cart_session.get('gift_cards'),
|
||||
shown_total=self.cart_session.get('shown_total'),
|
||||
customer=self.cart_session.get('customer'),
|
||||
)
|
||||
|
||||
def get_success_message(self, value):
|
||||
create_empty_cart_id(self.request)
|
||||
if isinstance(value, dict):
|
||||
for w in value.get('warnings', []):
|
||||
messages.warning(self.request, w)
|
||||
return None
|
||||
|
||||
def get_success_url(self, value):
|
||||
if isinstance(value, dict):
|
||||
value = value['order_id']
|
||||
order = Order.objects.get(id=value)
|
||||
return self.get_order_url(order)
|
||||
|
||||
|
||||
@@ -184,9 +184,10 @@ You will receive the request triggering the order creation as the ``request`` ke
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
checkout_confirm_page_content = EventPluginSignal()
|
||||
"""
|
||||
Arguments: 'request'
|
||||
Arguments: ``request``
|
||||
|
||||
This signals allows you to add HTML content to the confirmation page that is presented at the
|
||||
end of the checkout process, just before the order is being created.
|
||||
@@ -197,7 +198,7 @@ argument will contain the request object.
|
||||
|
||||
fee_calculation_for_cart = EventPluginSignal()
|
||||
"""
|
||||
Arguments: 'request', 'invoice_address', 'total', 'positions'
|
||||
Arguments: ``request``, ``invoice_address``, ``total``, ``positions``, ``payment_requetss``
|
||||
|
||||
This signals allows you to add fees to a cart. You are expected to return a list of ``OrderFee``
|
||||
objects that are not yet saved to the database (because there is no order yet).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% load eventurl %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}{% trans "Review order" %}{% endblock %}
|
||||
@@ -37,7 +38,7 @@
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=False %}
|
||||
</div>
|
||||
</div>
|
||||
{% if payment_provider %}
|
||||
{% if payments %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
{% if payment_provider.identifier != "free" %}
|
||||
@@ -52,9 +53,25 @@
|
||||
{% trans "Payment" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ payment }}
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
{% for payment, rendered_block in payments %}
|
||||
<li class="list-group-item payment">
|
||||
{% if payments|length > 1 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-10 col-xs-12">
|
||||
<h4>{{ payment.provider_name }}</h4>
|
||||
{{ rendered_block }}
|
||||
</div>
|
||||
<div class="col-sm-2 col-xs-12 text-right">
|
||||
<h4>{{ payment.payment_amount|money:request.event.currency }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ rendered_block }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% eventsignal event "pretix.presale.signals.checkout_confirm_page_content" request=request %}
|
||||
|
||||
@@ -4,73 +4,120 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block inner %}
|
||||
<p>{% trans "Please select how you want to pay." %}</p>
|
||||
{% if event.settings.payment_explanation %}
|
||||
{{ event.settings.payment_explanation|rich_text }}
|
||||
{% if current_payments %}
|
||||
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="list-group">
|
||||
{% for p in current_payments %}
|
||||
<div class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-xs-9">
|
||||
{{ p.provider_name }}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right">
|
||||
{{ p.payment_amount|money:request.event.currency }}
|
||||
</div>
|
||||
<div class="col-xs-1 text-right">
|
||||
<button name="remove_payment" value="{{ p.id }}" title="{% trans "Remove payment" %}"
|
||||
class="btn btn-link btn-xs">
|
||||
<span class="fa fa-trash text-danger"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if remaining %}
|
||||
<div class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-xs-9">
|
||||
<strong>{% trans "Remaining balance" %}</strong><br>
|
||||
<span class="text-muted">{% trans "Please select a payment method below." %}</span>
|
||||
</div>
|
||||
<div class="col-xs-2 text-right">
|
||||
<strong>
|
||||
{{ remaining|money:request.event.currency }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% if remaining %}
|
||||
<p>{% trans "Please select how you want to pay the remaining balance:" %}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% trans "Please select how you want to pay." %}</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="payment_accordion">
|
||||
{% for p in providers %}
|
||||
<div class="panel panel-default" data-total="{{ p.total|floatformat:2 }}">
|
||||
<div class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<p class="panel-title">
|
||||
{% if show_fees %}
|
||||
<strong class="pull-right flip">{% if p.fee < 0 %}-{% else %}+{% endif %} {{ p.fee|money:event.currency|cut:"-" }}</strong>
|
||||
{% endif %}
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
title="{{ p.provider.public_name }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
id="input_payment_{{ p.provider.identifier }}"
|
||||
aria-describedby="payment_{{ p.provider.identifier }}"
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/>
|
||||
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
||||
</p>
|
||||
{% if not current_payments or remaining %}
|
||||
{% if event.settings.payment_explanation %}
|
||||
{{ event.settings.payment_explanation|rich_text }}
|
||||
{% endif %}
|
||||
<div class="panel-group" id="payment_accordion">
|
||||
{% for p in providers %}
|
||||
<div class="panel panel-default">
|
||||
<div class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<p class="panel-title">
|
||||
{% if show_fees %}
|
||||
<strong class="pull-right flip">{% if p.fee < 0 %}-{% else %}+{% endif %} {{ p.fee|money:event.currency|cut:"-" }}</strong>
|
||||
{% endif %}
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
title="{{ p.provider.public_name }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
id="input_payment_{{ p.provider.identifier }}"
|
||||
aria-describedby="payment_{{ p.provider.identifier }}"
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/>
|
||||
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="payment_{{ p.provider.identifier }}"
|
||||
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if request.event.testmode %}
|
||||
{% if p.provider.test_mode_message %}
|
||||
<div class="alert alert-info">
|
||||
<p>{{ p.provider.test_mode_message }}</p>
|
||||
</div>
|
||||
{% if not request.sales_channel.testmode_supported %}
|
||||
<div class="alert alert-danger">
|
||||
<div id="payment_{{ p.provider.identifier }}"
|
||||
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if request.event.testmode %}
|
||||
{% if p.provider.test_mode_message %}
|
||||
<div class="alert alert-info">
|
||||
<p>{{ p.provider.test_mode_message }}</p>
|
||||
</div>
|
||||
{% if not request.sales_channel.testmode_supported %}
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{% trans "This sales channel does not provide support for test mode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, you might pay an actual order with non-existing money!" %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{% trans "This sales channel does not provide support for test mode." %}
|
||||
{% trans "This payment provider does not provide support for test mode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, you might pay an actual order with non-existing money!" %}
|
||||
{% trans "If you continue, actual money might be transferred." %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{% trans "This payment provider does not provide support for test mode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, actual money might be transferred." %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ p.form }}
|
||||
{{ p.form }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not providers %}
|
||||
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
|
||||
{% if not event.live %}
|
||||
<p>{% trans "Please go to the payment settings and activate one or more payment providers." %}</p>
|
||||
{% endfor %}
|
||||
{% if not providers %}
|
||||
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
|
||||
{% if not event.live %}
|
||||
<p>{% trans "Please go to the payment settings and activate one or more payment providers." %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -39,9 +39,11 @@ from functools import wraps
|
||||
from itertools import groupby
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Sum
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
@@ -50,6 +52,7 @@ from pretix.base.models import (
|
||||
QuestionAnswer, QuestionOption,
|
||||
)
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.signals import question_form_fields
|
||||
@@ -103,7 +106,7 @@ class CartMixin:
|
||||
def invoice_address(self):
|
||||
return cached_invoice_address(self.request)
|
||||
|
||||
def get_cart(self, answers=False, queryset=None, order=None, downloads=False):
|
||||
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
|
||||
if queryset is not None:
|
||||
prefetch = []
|
||||
if answers:
|
||||
@@ -196,7 +199,8 @@ class CartMixin:
|
||||
fees = order.fees.all()
|
||||
elif positions:
|
||||
fees = get_fees(
|
||||
self.request.event, self.request, total, self.invoice_address, self.cart_session.get('payment'),
|
||||
self.request.event, self.request, total, self.invoice_address,
|
||||
payments if payments is not None else self.cart_session.get('payments', []),
|
||||
cartpos
|
||||
)
|
||||
else:
|
||||
@@ -233,6 +237,57 @@ class CartMixin:
|
||||
'itemcount': sum(c.count for c in positions if not c.addon_to)
|
||||
}
|
||||
|
||||
def current_selected_payments(self, total, warn=False, total_includes_payment_fees=False):
|
||||
raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
|
||||
payments = []
|
||||
total_remaining = total
|
||||
for p in raw_payments:
|
||||
# This algorithm of treating min/max values and fees needs to stay in sync between the following
|
||||
# places in the code base:
|
||||
# - pretix.base.services.cart.get_fees
|
||||
# - pretix.base.services.orders._get_fees
|
||||
# - pretix.presale.views.CartMixin.current_selected_payments
|
||||
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
|
||||
if warn:
|
||||
messages.warning(
|
||||
self.request,
|
||||
_('Your selected payment method can only be used for a payment of at least {amount}.').format(
|
||||
amount=money_filter(Decimal(p['min_value']), self.request.event.currency)
|
||||
)
|
||||
)
|
||||
self._remove_payment(p['id'])
|
||||
continue
|
||||
|
||||
to_pay = total_remaining
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
pprov = self.request.event.get_payment_providers(cached=True).get(p['provider'])
|
||||
if not pprov:
|
||||
self._remove_payment(p['id'])
|
||||
continue
|
||||
|
||||
if not total_includes_payment_fees:
|
||||
fee = pprov.calculate_fee(to_pay)
|
||||
total_remaining += fee
|
||||
to_pay += fee
|
||||
else:
|
||||
fee = Decimal('0.00')
|
||||
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
p['payment_amount'] = to_pay
|
||||
p['provider_name'] = pprov.public_name
|
||||
p['pprov'] = pprov
|
||||
p['fee'] = fee
|
||||
total_remaining -= to_pay
|
||||
payments.append(p)
|
||||
return payments
|
||||
|
||||
def _remove_payment(self, payment_id):
|
||||
self.cart_session['payments'] = [p for p in self.cart_session['payments'] if p.get('id') != payment_id]
|
||||
|
||||
|
||||
def cart_exists(request):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
@@ -334,7 +389,7 @@ def get_cart_is_free(request):
|
||||
pos = get_cart(request)
|
||||
ia = get_cart_invoice_address(request)
|
||||
total = get_cart_total(request)
|
||||
fees = get_fees(request.event, request, total, ia, cs.get('payment'), pos)
|
||||
fees = get_fees(request.event, request, total, ia, cs.get('payments', []), pos)
|
||||
request._cart_free_cache = total + sum(f.value for f in fees) == Decimal('0.00')
|
||||
return request._cart_free_cache
|
||||
|
||||
|
||||
@@ -208,6 +208,14 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
|
||||
self.kwargs = kwargs
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.order.status == Order.STATUS_PENDING:
|
||||
payment_to_complete = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CREATED, process_initiated=False).first()
|
||||
if payment_to_complete:
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret,
|
||||
'payment': payment_to_complete.pk
|
||||
}))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
@@ -472,6 +480,8 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
'invoice': i.pk
|
||||
})
|
||||
messages.success(self.request, _('An invoice has been generated.'))
|
||||
self.payment.process_initiated = True
|
||||
self.payment.save(update_fields=['process_initiated'])
|
||||
resp = self.payment.payment_provider.execute_payment(request, self.payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
@@ -532,6 +542,8 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.payment.process_initiated = True
|
||||
self.payment.save(update_fields=['process_initiated'])
|
||||
resp = self.payment.payment_provider.execute_payment(request, self.payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
|
||||
Reference in New Issue
Block a user