diff --git a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html
index b6b05d74f7..aadaaf95df 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html
@@ -64,6 +64,14 @@
{{ t.order.full_code }}
+ {% if t.value > 0 and t.value <= card.value %}
+
+ {% endif %}
{% else %}
{% trans "Manual transaction" %}{% if t.text %}: {{ t.text }}{% endif %}
{% endif %}
diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py
index a8e2980679..923d0e9d01 100644
--- a/src/pretix/control/views/organizer.py
+++ b/src/pretix/control/views/organizer.py
@@ -23,11 +23,12 @@ from django.views.generic import (
from pretix.api.models import WebHook
from pretix.base.auth import get_auth_backends
from pretix.base.models import (
- Device, GiftCard, Organizer, Team, TeamInvite, User,
+ Device, GiftCard, OrderPayment, Organizer, Team, TeamInvite, User,
)
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
from pretix.base.models.giftcards import gen_giftcard_secret
from pretix.base.models.organizer import TeamAPIToken
+from pretix.base.payment import PaymentException
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.filter import (
EventFilterForm, GiftCardFilterForm, OrganizerFilterForm,
@@ -998,7 +999,36 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
@transaction.atomic()
def post(self, request, *args, **kwargs):
self.object = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
- if 'value' in request.POST:
+ if 'revert' in request.POST:
+ t = get_object_or_404(self.object.transactions.all(), pk=request.POST.get('revert'), order__isnull=False)
+ if self.object.value - t.value < Decimal('0.00'):
+ messages.error(request, _('Gift cards are not allowed to have negative values.'))
+ elif t.value > 0:
+ r = t.order.payments.create(
+ order=t.order,
+ state=OrderPayment.PAYMENT_STATE_CREATED,
+ amount=t.value,
+ provider='giftcard',
+ info=json.dumps({
+ 'gift_card': self.object.pk,
+ 'retry': True,
+ })
+ )
+ try:
+ r.payment_provider.execute_payment(None, r)
+ except PaymentException as e:
+ with transaction.atomic():
+ r.state = OrderPayment.PAYMENT_STATE_FAILED
+ r.save()
+ t.order.log_action('pretix.event.order.payment.failed', {
+ 'local_id': r.local_id,
+ 'provider': r.provider,
+ 'error': str(e)
+ })
+ messages.error(request, _('The transaction could not be reversed.'))
+ else:
+ messages.success(request, _('The transaction has been reversed.'))
+ elif 'value' in request.POST:
try:
value = DecimalField(localize=True).to_python(request.POST.get('value'))
except ValidationError:
diff --git a/src/tests/control/test_giftcards.py b/src/tests/control/test_giftcards.py
index 47c787bb89..c4b85473ef 100644
--- a/src/tests/control/test_giftcards.py
+++ b/src/tests/control/test_giftcards.py
@@ -1,6 +1,12 @@
-import pytest
+from datetime import timedelta
-from pretix.base.models import Organizer, Team, User
+import pytest
+from django.utils.timezone import now
+from django_scopes import scopes_disabled
+
+from pretix.base.models import (
+ Order, OrderPayment, OrderRefund, Organizer, Team, User,
+)
@pytest.fixture
@@ -83,6 +89,37 @@ def test_card_detail_view_transact(organizer, admin_user, gift_card, client):
assert gift_card.all_logentries().count() == 1
+@pytest.mark.django_db
+def test_card_detail_view_transact_revert_refund(organizer, admin_user, gift_card, client):
+ with scopes_disabled():
+ event = organizer.events.create(
+ name='Dummy', slug='dummy',
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.stripe,tests.testdummy'
+ )
+ o = Order.objects.create(
+ code='FOO', event=event, email='dummy@dummy.test',
+ status=Order.STATUS_CANCELED,
+ datetime=now(), expires=now() + timedelta(days=10),
+ total=14, locale='en'
+ )
+ o.payments.create(
+ amount=o.total, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CONFIRMED
+ )
+ r = o.refunds.create(
+ amount=o.total, provider='giftcard', state=OrderRefund.REFUND_STATE_DONE
+ )
+ t = gift_card.transactions.create(value=14, order=o, refund=r)
+
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ r = client.post('/control/organizer/dummy/giftcard/{}/'.format(gift_card.pk), {
+ 'revert': str(t.pk)
+ })
+ assert 'alert-success' in r.rendered_content
+ assert gift_card.value == 42
+ o.refresh_from_db()
+ assert o.pending_sum == -14
+
+
@pytest.mark.django_db
def test_card_detail_view_transact_min_value(organizer, admin_user, gift_card, client):
client.login(email='dummy@dummy.dummy', password='dummy')