mirror of
https://github.com/pretix/pretix.git
synced 2026-05-10 16:04:02 +00:00
Refator payment provider, deal with cancellations
This commit is contained in:
@@ -458,11 +458,15 @@ class Order(LockModel, LoggedModel):
|
|||||||
positions = list(
|
positions = list(
|
||||||
self.positions.all().annotate(
|
self.positions.all().annotate(
|
||||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
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])
|
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
|
||||||
if not cancelable or not positions:
|
if not cancelable or not positions:
|
||||||
return False
|
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:
|
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
||||||
return False
|
return False
|
||||||
if self.status == Order.STATUS_PENDING:
|
if self.status == Order.STATUS_PENDING:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Any, Dict, Union
|
|||||||
import pytz
|
import pytz
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -21,8 +22,8 @@ from i18nfield.strings import LazyI18nString
|
|||||||
|
|
||||||
from pretix.base.forms import PlaceholderValidator
|
from pretix.base.forms import PlaceholderValidator
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, InvoiceAddress, Order, OrderPayment, OrderRefund,
|
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
|
||||||
Quota,
|
OrderRefund, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||||
from pretix.base.settings import SettingsSandbox
|
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.money import money_filter
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.helpers.money import DecimalTextInput
|
from pretix.helpers.money import DecimalTextInput
|
||||||
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
from pretix.presale.views import get_cart_total
|
from pretix.presale.views import get_cart_total
|
||||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||||
|
|
||||||
@@ -890,12 +892,11 @@ class OffsettingProvider(BasePaymentProvider):
|
|||||||
|
|
||||||
|
|
||||||
class GiftCardPayment(BasePaymentProvider):
|
class GiftCardPayment(BasePaymentProvider):
|
||||||
is_enabled = True
|
|
||||||
identifier = "giftcard"
|
identifier = "giftcard"
|
||||||
verbose_name = _("Gift card")
|
verbose_name = _("Gift card")
|
||||||
|
|
||||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
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:
|
def order_change_allowed(self, order: Order) -> bool:
|
||||||
return False
|
return False
|
||||||
@@ -903,6 +904,9 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
|
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||||
raise PaymentException("Invalid state, should never occur.")
|
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:
|
def payment_control_render(self, request, payment) -> str:
|
||||||
from .models import GiftCard
|
from .models import GiftCard
|
||||||
|
|
||||||
@@ -933,6 +937,42 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||||
return True
|
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()
|
@transaction.atomic()
|
||||||
def execute_refund(self, refund: OrderRefund):
|
def execute_refund(self, refund: OrderRefund):
|
||||||
from .models import GiftCard
|
from .models import GiftCard
|
||||||
|
|||||||
@@ -329,6 +329,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
if position.voucher:
|
if position.voucher:
|
||||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
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,
|
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
||||||
data={'cancellation_fee': cancellation_fee})
|
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")
|
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
||||||
|
any_giftcards = False
|
||||||
for p in order.positions.all():
|
for p in order.positions.all():
|
||||||
if p.item.issue_giftcard:
|
if p.item.issue_giftcard:
|
||||||
gc = sender.organizer.issued_gift_cards.create(
|
gc = sender.organizer.issued_gift_cards.create(
|
||||||
currency=sender.currency, issued_in=p
|
currency=sender.currency, issued_in=p
|
||||||
)
|
)
|
||||||
gc.transactions.create(value=p.price, order=order)
|
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})
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ DEFAULTS = {
|
|||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool
|
'type': bool
|
||||||
},
|
},
|
||||||
|
'payment_giftcard__enabled': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
'payment_term_accept_late': {
|
'payment_term_accept_late': {
|
||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool
|
'type': bool
|
||||||
|
|||||||
@@ -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" %}">
|
||||||
@@ -897,23 +897,26 @@ class OrderTransition(OrderView):
|
|||||||
else:
|
else:
|
||||||
messages.success(self.request, _('The payment has been created successfully.'))
|
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():
|
elif self.order.cancel_allowed() and to == 'c' and self.mark_canceled_form.is_valid():
|
||||||
cancel_order(self.order, user=self.request.user,
|
try:
|
||||||
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
|
cancel_order(self.order, user=self.request.user,
|
||||||
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
|
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
|
||||||
self.order.refresh_from_db()
|
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.'))
|
||||||
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.'))
|
|
||||||
elif self.order.status == Order.STATUS_PENDING and to == 'e':
|
elif self.order.status == Order.STATUS_PENDING and to == 'e':
|
||||||
mark_order_expired(self.order, user=self.request.user)
|
mark_order_expired(self.order, user=self.request.user)
|
||||||
messages.success(self.request, _('The order has been marked as expired.'))
|
messages.success(self.request, _('The order has been marked as expired.'))
|
||||||
|
|||||||
@@ -15,13 +15,12 @@ from django.utils.translation import (
|
|||||||
from django.views.generic.base import TemplateResponseMixin
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
from django_scopes import scopes_disabled
|
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.models.orders import InvoiceAddress, OrderPayment
|
||||||
from pretix.base.services.cart import (
|
from pretix.base.services.cart import (
|
||||||
get_fees, set_cart_addons, update_tax_rates,
|
get_fees, set_cart_addons, update_tax_rates,
|
||||||
)
|
)
|
||||||
from pretix.base.services.orders import perform_order
|
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.base.views.tasks import AsyncAction
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
from pretix.presale.forms.checkout import (
|
from pretix.presale.forms.checkout import (
|
||||||
@@ -531,40 +530,6 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
self.request = 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:
|
for p in self.provider_forms:
|
||||||
if p['provider'].identifier == request.POST.get('payment', ''):
|
if p['provider'].identifier == request.POST.get('payment', ''):
|
||||||
self.cart_session['payment'] = p['provider'].identifier
|
self.cart_session['payment'] = p['provider'].identifier
|
||||||
@@ -589,7 +554,6 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
|||||||
if len(self.provider_forms) == 1:
|
if len(self.provider_forms) == 1:
|
||||||
ctx['selected'] = self.provider_forms[0]['provider'].identifier
|
ctx['selected'] = self.provider_forms[0]['provider'].identifier
|
||||||
ctx['cart'] = self.get_cart()
|
ctx['cart'] = self.get_cart()
|
||||||
ctx['has_gift_cards'] = self.request.organizer.has_gift_cards
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|||||||
@@ -51,33 +51,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% if not providers %}
|
||||||
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
|
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
|
||||||
{% if not event.live %}
|
{% if not event.live %}
|
||||||
|
|||||||
Reference in New Issue
Block a user