Repository: pretix/pretix

Issue:
If a voucher was used several times in one order, show order only once
pretix warns you if a voucher was used in an order when viewing the voucher. If the voucher was redeemed several times in the same order, you'll get repeated links to the same order, which is amusing, but not necessary.


Discussion:

Resolving patch:
diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py
--- a/src/pretix/base/models/vouchers.py
+++ b/src/pretix/base/models/vouchers.py
@@ -13,6 +13,7 @@
 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 @@ def calculate_price(self, original_price: Decimal) -> Decimal:
                 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()


Test patch:
diff --git a/src/tests/control/test_vouchers.py b/src/tests/control/test_vouchers.py
--- 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 @@ def test_subevent_blocking_quota_full(self):
             '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