diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 0aa8450f5..617cf815b 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -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: diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index e3cfe4dac..848c65ecb 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -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 diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 651cba36a..29a5dd780 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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}) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 94ae8fbef..790d346a3 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -133,6 +133,10 @@ DEFAULTS = { 'default': 'True', 'type': bool }, + 'payment_giftcard__enabled': { + 'default': 'True', + 'type': bool + }, 'payment_term_accept_late': { 'default': 'True', 'type': bool diff --git a/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html b/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html new file mode 100644 index 000000000..b5b1a0834 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html @@ -0,0 +1,10 @@ +{% load i18n %} + +

+ {% 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 %} +

+ diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 75e1580d6..b0004f061 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -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.')) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 319648bd2..a0f9c0c8d 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -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 diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html index 53f1ff9ba..21d10ee53 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html @@ -51,33 +51,6 @@ {% endfor %} - {% if has_gift_cards %} -
- - -
- {% endif %} {% if not providers %}

{% trans "There are no payment providers enabled." %}

{% if not event.live %}