diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py
index 61104be6e5..6049e0ebf3 100644
--- a/src/pretix/base/models/vouchers.py
+++ b/src/pretix/base/models/vouchers.py
@@ -13,6 +13,7 @@ from ..decimal import round_decimal
from .base import LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Quota
+from .orders import Order
def _generate_random_code(prefix=None):
@@ -380,3 +381,11 @@ class Voucher(LoggedModel):
return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
return p
return original_price
+
+ def distinct_orders(self):
+ """
+ Return the list of orders where this voucher has been used.
+ Each order will appear at most once.
+ """
+
+ return Order.objects.filter(all_positions__voucher__in=[self]).distinct()
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
index fa5461382f..ff7e613e2f 100644
--- a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
+++ b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
@@ -9,9 +9,9 @@
{% trans "This voucher already has been used. It is not recommended to modify it." %}
diff --git a/src/tests/control/test_vouchers.py b/src/tests/control/test_vouchers.py
index 60908a1667..59e7bf1e8b 100644
--- a/src/tests/control/test_vouchers.py
+++ b/src/tests/control/test_vouchers.py
@@ -1,11 +1,13 @@
import datetime
+import decimal
import json
from django.utils.timezone import now
from tests.base import SoupTest, extract_form_fields
from pretix.base.models import (
- Event, Item, ItemVariation, Organizer, Quota, Team, User, Voucher,
+ Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota, Team,
+ User, Voucher,
)
@@ -517,3 +519,39 @@ class VoucherFormTest(SoupTest):
'block_quota': 'on',
'subevent': se1.pk
}, expected_failure=True)
+
+ def test_order_warning_deduplication(self):
+ shirt_voucher = Voucher.objects.create(
+ event=self.event, item=self.shirt, price_mode='set', value=0.0, max_usages=100
+ )
+
+ shirt_order = Order.objects.create(
+ code='DEDUP', event=self.event, email='dummy@dummy.test',
+ status=Order.STATUS_PAID,
+ datetime=now(), expires=now() + datetime.timedelta(days=10),
+ total=0, locale='en'
+ )
+
+ OrderPosition.objects.create(
+ order=shirt_order,
+ item=self.shirt,
+ variation=self.shirt_red,
+ price=decimal.Decimal("0"),
+ voucher=shirt_voucher
+ )
+
+ OrderPosition.objects.create(
+ order=shirt_order,
+ item=self.shirt,
+ variation=self.shirt_blue,
+ price=decimal.Decimal("0"),
+ voucher=shirt_voucher
+ )
+
+ shirt_voucher.redeemed = 2
+ shirt_voucher.save()
+
+ doc = self.get_doc('/control/event/%s/%s/vouchers/%s/' % (self.orga.slug, self.event.slug, shirt_voucher.pk))
+
+ assert len(doc.select('.alert-warning ul li')) == 1 # Check that there's exactly 1 item in the warning list
+ assert doc.text.count('Order DEDUP') == 1 # Check that the order is listed exactly once