Refator payment provider, deal with cancellations

This commit is contained in:
Raphael Michel
2019-09-19 10:06:26 +02:00
parent e099fad0ca
commit 346f215c50
8 changed files with 99 additions and 85 deletions

View File

@@ -458,11 +458,15 @@ class Order(LockModel, LoggedModel):
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item')
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
for gc in op.issued_gift_cards.all():
if gc.value != op.price:
return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PENDING:

View File

@@ -7,6 +7,7 @@ from typing import Any, Dict, Union
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.dispatch import receiver
@@ -21,8 +22,8 @@ from i18nfield.strings import LazyI18nString
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Order, OrderPayment, OrderRefund,
Quota,
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -30,6 +31,7 @@ from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
@@ -890,12 +892,11 @@ class OffsettingProvider(BasePaymentProvider):
class GiftCardPayment(BasePaymentProvider):
is_enabled = True
identifier = "giftcard"
verbose_name = _("Gift card")
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
return super().is_allowed(request, total) and self.event.organizer.has_gift_cards
def order_change_allowed(self, order: Order) -> bool:
return False
@@ -903,6 +904,9 @@ class GiftCardPayment(BasePaymentProvider):
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
raise PaymentException("Invalid state, should never occur.")
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
return get_template('pretixcontrol/giftcards/checkout.html').render({})
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
@@ -933,6 +937,42 @@ class GiftCardPayment(BasePaymentProvider):
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return True
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
cs = cart_session(request)
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
if 'gift_cards' not in cs:
cs['gift_cards'] = []
elif gc.pk in cs['gift_cards']:
messages.error(request, _("This gift card is already used for your payment."))
return
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
remainder = cart['total'] - gc.value
if remainder >= Decimal('0.00'):
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
money_filter(remainder, self.event.currency)
))
else:
messages.success(request, _("Your gift card has been applied."))
kwargs = {'step': 'payment'}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
return eventreverse(self.event, 'presale:event.checkout', kwargs=kwargs)
except GiftCard.DoesNotExist:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
@transaction.atomic()
def execute_refund(self, refund: OrderRefund):
from .models import GiftCard

View File

@@ -329,6 +329,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
for position in order.positions.all():
for gc in position.issued_gift_cards.all():
if gc.value < position.price:
raise OrderError(_('This order can not be canceled since the gift card {card} purchased in this order has already been redeemed.').format(
card=gc.secret
))
else:
gc.transactions.create(value=-position.price, order=order)
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee})
@@ -1659,9 +1668,16 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
@transaction.atomic()
def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
any_giftcards = False
for p in order.positions.all():
if p.item.issue_giftcard:
gc = sender.organizer.issued_gift_cards.create(
currency=sender.currency, issued_in=p
)
gc.transactions.create(value=p.price, order=order)
any_giftcards = True
p.secret = gc.secret
p.save(update_fields=['secret'])
if any_giftcards:
tickets.invalidate_cache.apply_async(kwargs={'event': sender.pk, 'order': order.pk})

View File

@@ -133,6 +133,10 @@ DEFAULTS = {
'default': 'True',
'type': bool
},
'payment_giftcard__enabled': {
'default': 'True',
'type': bool
},
'payment_term_accept_late': {
'default': 'True',
'type': bool

View File

@@ -0,0 +1,10 @@
{% load i18n %}
<p>
{% blocktrans %}
If you have a gift card, please enter the gift card code here. If the gift card does not have
enough credit to pay for the full order, you will be shown this page again and you can either
redeem another gift card or select a different payment method for the difference.
{% endblocktrans %}
</p>
<input name="giftcard" class="form-control" placeholder="{% trans "Gift card code" %}">

View File

@@ -897,23 +897,26 @@ class OrderTransition(OrderView):
else:
messages.success(self.request, _('The payment has been created successfully.'))
elif self.order.cancel_allowed() and to == 'c' and self.mark_canceled_form.is_valid():
cancel_order(self.order, user=self.request.user,
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
self.order.refresh_from_db()
try:
cancel_order(self.order, user=self.request.user,
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
except OrderError as e:
messages.error(self.request, str(e))
else:
self.order.refresh_from_db()
if self.order.pending_sum < 0:
messages.success(self.request, _('The order has been canceled. You can now select how you want to '
'transfer the money back to the user.'))
return redirect(reverse('control:event.order.refunds.start', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}'.format(
self.order.pending_sum * -1
))
if self.order.pending_sum < 0:
messages.success(self.request, _('The order has been canceled. You can now select how you want to '
'transfer the money back to the user.'))
return redirect(reverse('control:event.order.refunds.start', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}'.format(
self.order.pending_sum * -1
))
messages.success(self.request, _('The order has been canceled.'))
messages.success(self.request, _('The order has been canceled.'))
elif self.order.status == Order.STATUS_PENDING and to == 'e':
mark_order_expired(self.order, user=self.request.user)
messages.success(self.request, _('The order has been marked as expired.'))

View File

@@ -15,13 +15,12 @@ from django.utils.translation import (
from django.views.generic.base import TemplateResponseMixin
from django_scopes import scopes_disabled
from pretix.base.models import GiftCard, Order
from pretix.base.models import Order
from pretix.base.models.orders import InvoiceAddress, OrderPayment
from pretix.base.services.cart import (
get_fees, set_cart_addons, update_tax_rates,
)
from pretix.base.services.orders import perform_order
from pretix.base.templatetags.money import money_filter
from pretix.base.views.tasks import AsyncAction
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import (
@@ -531,40 +530,6 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
def post(self, request):
self.request = request
if request.POST.get("giftcard") and request.POST.get("payment") == "giftcard":
# TODO: cross-organizer acceptance, …
try:
gc = request.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != request.event.currency:
messages.error(self.request, _("This gift card does not support this currency."))
return self.render()
if gc.value <= Decimal("0.00"):
messages.error(self.request, _("All credit on this gift card has been used."))
return self.render()
if 'gift_cards' not in self.cart_session:
self.cart_session['gift_cards'] = []
elif gc.pk in self.cart_session['gift_cards']:
messages.error(self.request, _("This gift card is already used for your payment."))
return self.render()
self.cart_session['gift_cards'] = self.cart_session['gift_cards'] + [gc.pk]
remainder = self._total_order_value - gc.value
if remainder >= Decimal('0.00'):
messages.success(self.request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
money_filter(remainder, self.event.currency)
))
else:
messages.success(self.request, _("Your gift card has been applied."))
return redirect(self.get_step_url(request))
except GiftCard.DoesNotExist:
messages.error(self.request, _("This gift card is not known."))
return self.render()
except GiftCard.MultipleObjectsReturned:
messages.error(self.request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
return self.render()
for p in self.provider_forms:
if p['provider'].identifier == request.POST.get('payment', ''):
self.cart_session['payment'] = p['provider'].identifier
@@ -589,7 +554,6 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
ctx['cart'] = self.get_cart()
ctx['has_gift_cards'] = self.request.organizer.has_gift_cards
return ctx
@cached_property

View File

@@ -51,33 +51,6 @@
</div>
</div>
{% endfor %}
{% if has_gift_cards %}
<div class="panel panel-default">
<label class="accordion-radio">
<div class="panel-heading">
<h4 class="panel-title">
<input type="radio" name="payment" value="giftcard"
data-parent="#payment_accordion"
data-toggle="radiocollapse" data-target="#payment_giftcard"/>
<strong>{% trans "Redeem a gift card" %}</strong>
</h4>
</div>
</label>
<div id="payment_giftcard"
class="panel-collapse collapsed">
<div class="panel-body form-horizontal">
<p>
{% blocktrans %}
If you have a gift card, please enter the gift card code here. If the gift card does not have
enough credit to pay for the full order, you will be shown this page again and you can either
redeem another gift card or select a different payment method for the difference.
{% endblocktrans %}
</p>
<input name="giftcard" class="form-control" placeholder="{% trans "Gift card code" %}">
</div>
</div>
</div>
{% endif %}
{% if not providers %}
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
{% if not event.live %}