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():