From 5e97f668a549b0c1cbf94e02220d7cc321fccf24 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 26 Jan 2026 09:29:41 +0100 Subject: [PATCH] Order data export: Allow to filter by product (Z#23212618) (#5826) * Order data export: Allow to filter by product (Z#23212618) * Fix tests --- src/pretix/base/exporters/orderlist.py | 37 ++++++++++++++++++++++++-- src/tests/api/test_exporters.py | 16 ++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index c3fcf72b19..5df96a12d2 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -39,8 +39,8 @@ from zoneinfo import ZoneInfo from django import forms from django.conf import settings from django.db.models import ( - Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef, - Q, Subquery, Sum, When, + Case, CharField, Count, DateTimeField, Exists, F, IntegerField, Max, Min, + OuterRef, Q, Subquery, Sum, When, ) from django.db.models.functions import Coalesce from django.dispatch import receiver @@ -144,6 +144,18 @@ class OrderListExporter(MultiSheetListExporter): d = OrderedDict(d) if not self.is_multievent and not self.event.has_subevents: del d['event_date_range'] + if not self.is_multievent: + d["items"] = forms.ModelMultipleChoiceField( + label=_("Products"), + queryset=self.event.items.all(), + widget=forms.CheckboxSelectMultiple( + attrs={"class": "scrolling-multiple-choice"} + ), + help_text=_("If none are selected, all products are included. Orders are included if they contain " + "at least one position of this product. The order totals etc. still include all products " + "contained in the order."), + required=False, + ) return d def _get_all_payment_methods(self, qs): @@ -249,6 +261,14 @@ class OrderListExporter(MultiSheetListExporter): pcnt=Subquery(s, output_field=IntegerField()) ).select_related('invoice_address', 'customer') + if form_data.get('items'): + qs = qs.filter( + Exists(OrderPosition.all.filter( + order=OuterRef('pk'), + item__in=form_data["items"] + )) + ) + qs = self._date_filter(qs, form_data, rel='') if form_data['paid_only']: @@ -440,6 +460,14 @@ class OrderListExporter(MultiSheetListExporter): if form_data['paid_only']: qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False) + if form_data.get('items'): + qs = qs.filter( + Exists(OrderPosition.all.filter( + order=OuterRef('order'), + item__in=form_data["items"] + )) + ) + qs = self._date_filter(qs, form_data, rel='order__') return qs @@ -535,6 +563,11 @@ class OrderListExporter(MultiSheetListExporter): if form_data['paid_only']: qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False) + if form_data.get('items'): + qs = qs.filter( + item__in=form_data["items"] + ) + qs = self._date_filter(qs, form_data, rel='order__') return qs diff --git a/src/tests/api/test_exporters.py b/src/tests/api/test_exporters.py index a7fdb45e16..222362e6a1 100644 --- a/src/tests/api/test_exporters.py +++ b/src/tests/api/test_exporters.py @@ -82,6 +82,10 @@ SAMPLE_EXPORTER_CONFIG = { "name": "event_date_range", "required": False }, + { + "name": "items", + "required": False + }, ] } @@ -107,6 +111,10 @@ def test_org_list(token_client, organizer, event): "name": "events", "required": False }) + c['input_parameters'].remove({ + "name": "items", + "required": False + }) resp = token_client.get('/api/v1/organizers/{}/exporters/'.format(organizer.slug)) assert resp.status_code == 200 assert c in resp.data['results'] @@ -389,7 +397,7 @@ def test_event_scheduled_export_create(user_client, organizer, event, user): '/api/v1/organizers/{}/events/{}/scheduled_exports/'.format(organizer.slug, event.slug), data={ "export_identifier": "orderlist", - "export_form_data": {"_format": "xlsx", "date_range": "year_this"}, + "export_form_data": {"_format": "xlsx", "date_range": "year_this", "items": []}, "locale": "en", "mail_additional_recipients": "foo@example.org", "mail_additional_recipients_cc": "", @@ -403,7 +411,7 @@ def test_event_scheduled_export_create(user_client, organizer, event, user): ) assert resp.status_code == 201 created = event.scheduled_exports.get(id=resp.data["id"]) - assert created.export_form_data == {"_format": "xlsx", "date_range": "year_this"} + assert created.export_form_data == {"_format": "xlsx", "date_range": "year_this", "items": []} assert created.owner == user assert created.schedule_next_run > now() @@ -414,7 +422,7 @@ def test_event_scheduled_export_create_requires_user(token_client, organizer, ev '/api/v1/organizers/{}/events/{}/scheduled_exports/'.format(organizer.slug, event.slug), data={ "export_identifier": "orderlist", - "export_form_data": {"_format": "xlsx", "date_range": "year_this"}, + "export_form_data": {"_format": "xlsx", "date_range": "year_this", "items": []}, "locale": "en", "mail_additional_recipients": "foo@example.org", "mail_additional_recipients_cc": "", @@ -453,7 +461,7 @@ def test_event_scheduled_export_update_token(token_client, organizer, event, use ) assert resp.status_code == 200 created = event.scheduled_exports.get(id=resp.data["id"]) - assert created.export_form_data == {"_format": "xlsx", "date_range": "month_this"} + assert created.export_form_data == {"_format": "xlsx", "date_range": "month_this", "items": []} @pytest.fixture