diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 0f229c1d83..3cd63d4104 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -1149,7 +1149,7 @@ class GiftCardPayment(BasePaymentProvider): @transaction.atomic() def execute_refund(self, refund: OrderRefund): from .models import GiftCard - gc = GiftCard.objects.get(pk=refund.payment.info_data.get('gift_card')) + gc = GiftCard.objects.get(pk=refund.info_data.get('gift_card') or refund.payment.info_data.get('gift_card')) trans = gc.transactions.create( value=refund.amount, order=refund.order, diff --git a/src/pretix/control/templates/pretixcontrol/order/refund_choose.html b/src/pretix/control/templates/pretixcontrol/order/refund_choose.html index a8f3b9695f..9937cea3b4 100644 --- a/src/pretix/control/templates/pretixcontrol/order/refund_choose.html +++ b/src/pretix/control/templates/pretixcontrol/order/refund_choose.html @@ -83,6 +83,27 @@ value="" title="" class="form-control"> + + + + + + {% trans "Create a new gift card" %} + + + +
+ + + {{ request.event.currency }} + +
+
+ {% trans "The gift card can be used to buy tickets for all events of this organizer." %} +
+ + diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 8240e8aba5..f4fecfd121 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -5,6 +5,7 @@ import os import re from datetime import datetime, time, timedelta from decimal import Decimal, DecimalException +from urllib.parse import urlencode import vat_moss.id from django.conf import settings @@ -709,6 +710,33 @@ class OrderRefundView(OrderView): provider='manual' )) + giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0' + giftcard_value = formats.sanitize_separators(giftcard_value) + try: + giftcard_value = Decimal(giftcard_value) + except (DecimalException, TypeError): + messages.error(self.request, _('You entered an invalid number.')) + is_valid = False + else: + if giftcard_value: + refund_selected += giftcard_value + giftcard = self.request.organizer.issued_gift_cards.create( + currency=self.request.event.currency, + testmode=self.order.testmode + ) + refunds.append(OrderRefund( + order=self.order, + payment=None, + source=OrderRefund.REFUND_SOURCE_ADMIN, + state=OrderRefund.REFUND_STATE_CREATED, + execution_date=now(), + amount=giftcard_value, + provider='giftcard', + info=json.dumps({ + 'gift_card': giftcard.pk + }) + )) + offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0' offsetting_value = formats.sanitize_separators(offsetting_value) try: @@ -779,7 +807,7 @@ class OrderRefundView(OrderView): 'local_id': r.local_id, 'provider': r.provider, }, user=self.request.user) - if r.payment or r.provider == "offsetting": + if r.payment or r.provider == "offsetting" or r.provider == "giftcard": try: r.payment_provider.execute_refund(r) except PaymentException as e: @@ -816,6 +844,23 @@ class OrderRefundView(OrderView): ) self.order.save(update_fields=['status', 'expires']) + if giftcard_value and self.order.email: + messages.success(self.request, _('A new gift card was created. You can now send the user their ' + 'gift card code.')) + return redirect(reverse('control:event.order.sendmail', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + 'code': self.order.code + }) + '?' + urlencode({ + 'subject': _('Your gift card code'), + 'message': _('Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift ' + 'card code {giftcard} to pay for future ticket purchases in our shop.\n\n' + 'Your {event} team').format( + event="{event}", + amount=money_filter(giftcard_value, self.request.event.currency), + giftcard=giftcard.secret, + ) + })) return redirect(self.get_order_url()) else: messages.error(self.request, _('The refunds you selected do not match the selected total refund ' @@ -1563,6 +1608,11 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView): event=self.request.event, code=self.kwargs['code'].upper() ) + kwargs['initial'] = {} + if self.request.GET.get('subject'): + kwargs['initial']['subject'] = self.request.GET.get('subject') + if self.request.GET.get('message'): + kwargs['initial']['message'] = self.request.GET.get('message') return kwargs def form_invalid(self, form): diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index cf250d9bed..2f0599be5b 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -932,7 +932,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi def get_queryset(self): qs = self.request.organizer.issued_gift_cards.annotate( - cached_value=Sum('transactions__value') + cached_value=Coalesce(Sum('transactions__value'), Decimal('0.00')) ) if self.filter_form.is_valid(): qs = self.filter_form.filter_qs(qs) diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index c29104227a..8d44bb489c 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -12,8 +12,9 @@ from tests.base import SoupTest from tests.plugins.stripe.test_provider import MockedCharge from pretix.base.models import ( - Event, InvoiceAddress, Item, Order, OrderFee, OrderPayment, OrderPosition, - OrderRefund, Organizer, Question, QuestionAnswer, Quota, Team, User, + Event, GiftCard, InvoiceAddress, Item, Order, OrderFee, OrderPayment, + OrderPosition, OrderRefund, Organizer, Question, QuestionAnswer, Quota, + Team, User, ) from pretix.base.payment import PaymentException from pretix.base.services.invoices import ( @@ -2046,6 +2047,34 @@ def test_refund_paid_order_offsetting(client, env): assert p2.state == OrderPayment.PAYMENT_STATE_CONFIRMED +@pytest.mark.django_db +def test_refund_paid_order_giftcard(client, env): + with scopes_disabled(): + p = env[2].payments.last() + p.confirm() + client.login(email='dummy@dummy.dummy', password='dummy') + + client.post('/control/event/dummy/dummy/orders/FOO/refund', { + 'start-partial_amount': '5.00', + 'start-mode': 'partial', + 'start-action': 'mark_pending', + 'refund-new-giftcard': '5.00', + 'manual_state': 'pending', + 'perform': 'on' + }, follow=True) + p.refresh_from_db() + assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED + env[2].refresh_from_db() + with scopes_disabled(): + r = env[2].refunds.last() + assert r.provider == "giftcard" + assert r.state == OrderRefund.REFUND_STATE_DONE + assert r.amount == Decimal('5.00') + assert env[2].status == Order.STATUS_PENDING + gk = GiftCard.objects.get(pk=r.info_data['gift_card']) + assert gk.value == Decimal('5.00') + + @pytest.mark.django_db def test_refund_list(client, env): with scopes_disabled():