Repository: pretix/pretix

Issue:
Order page shows only one gift card code
When ordering multiple gift cards in a single order, only a single gift card code is shown on the order page after payment , see images attached. Is this intended?
Frontend:
![image](https://user-images.githubusercontent.com/12897721/89043958-98ed8e00-d349-11ea-89f3-7b75806a2316.png)
Backend:
![image](https://user-images.githubusercontent.com/12897721/89044377-2a5d0000-d34a-11ea-83cb-a5254eb72f2c.png)

Order page shows only one gift card code
When ordering multiple gift cards in a single order, only a single gift card code is shown on the order page after payment , see images attached. Is this intended?
Frontend:
![image](https://user-images.githubusercontent.com/12897721/89043958-98ed8e00-d349-11ea-89f3-7b75806a2316.png)
Backend:
![image](https://user-images.githubusercontent.com/12897721/89044377-2a5d0000-d34a-11ea-83cb-a5254eb72f2c.png)



Discussion:
It is not. We need to exclude gift cards from the grouping of positions apparently.
It is not. We need to exclude gift cards from the grouping of positions apparently.

Resolving patch:
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py
--- a/src/pretix/presale/views/__init__.py
+++ b/src/pretix/presale/views/__init__.py
@@ -112,16 +112,28 @@ def keyfunc(pos):
                 or self.request.event.settings.attendee_emails_asked
                 or pos_additional_fields.get(pos.pk)
             )
+
             addon_penalty = 1 if pos.addon_to_id else 0
-            if downloads or pos.pk in has_addons or pos.addon_to_id:
-                return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0), (pos.seat_id or 0)
-            if answers and (has_attendee_data or pos.item.questions.all()):
-                return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0), (pos.seat_id or 0)
 
+            if downloads \
+                    or pos.pk in has_addons \
+                    or pos.addon_to_id \
+                    or pos.item.issue_giftcard \
+                    or (answers and (has_attendee_data or pos.item.questions.exists())):
+                return (
+                    # standalone positions are grouped by main product position id, addons below them also sorted by position id
+                    i, addon_penalty, pos.pk,
+                    # all other places are only used for positions that can be grouped. We just put zeros.
+                ) + (0, ) * 10
+
+            # positions are sorted and grouped by various attributes
+            category_key = (pos.item.category.position, pos.item.category.id) if pos.item.category is not None else (0, 0)
+            item_key = pos.item.position, pos.item_id
+            variation_key = (pos.variation.position, pos.variation.id) if pos.variation is not None else (0, 0)
             return (
-                0, addon_penalty, 0, pos.item_id, (pos.variation_id or 0), pos.price, (pos.voucher_id or 0),
-                (pos.subevent_id or 0), (pos.seat_id or 0)
-            )
+                # These are grouped by attributes so we don't put any position ids
+                0, 0, 0,
+            ) + category_key + item_key + variation_key + (pos.price, (pos.voucher_id or 0), (pos.subevent_id or 0), (pos.seat_id or 0))
 
         positions = []
         for k, g in groupby(sorted(lcp, key=keyfunc), key=keyfunc):


Test patch: