From 7d5170155a0e4a17d8230ea731ec48fe3e353f2a Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 29 Mar 2021 12:05:56 +0200 Subject: [PATCH] Add filter for date for multiple exporters --- src/pretix/base/exporters/orderlist.py | 136 ++++++++++++++---- src/pretix/plugins/badges/exporters.py | 36 ++++- .../plugins/ticketoutputpdf/exporters.py | 40 ++++++ src/pretix/presale/views/event.py | 4 +- src/pretix/presale/views/organizer.py | 2 +- 5 files changed, 190 insertions(+), 28 deletions(-) diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index cf433a955..684aab67f 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -1,13 +1,14 @@ from collections import OrderedDict from decimal import Decimal +import dateutil import pytz from django import forms from django.db.models import ( - CharField, Count, DateTimeField, IntegerField, Max, OuterRef, Q, Subquery, - Sum, + Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef, + Q, Subquery, Sum, When, ) -from django.db.models.functions import Coalesce +from django.db.models.functions import Coalesce, TruncDate from django.dispatch import receiver from django.utils.functional import cached_property from django.utils.timezone import get_current_timezone, now @@ -48,28 +49,61 @@ class OrderListExporter(MultiSheetListExporter): @property def additional_form_fields(self): - return OrderedDict( - [ - ('paid_only', - forms.BooleanField( - label=_('Only paid orders'), - initial=True, - required=False - )), - ('include_payment_amounts', - forms.BooleanField( - label=_('Include payment amounts'), - initial=False, - required=False - )), - ('group_multiple_choice', - forms.BooleanField( - label=_('Show multiple choice answers grouped in one column'), - initial=False, - required=False - )), - ] - ) + d = [ + ('paid_only', + forms.BooleanField( + label=_('Only paid orders'), + initial=True, + required=False + )), + ('include_payment_amounts', + forms.BooleanField( + label=_('Include payment amounts'), + initial=False, + required=False + )), + ('group_multiple_choice', + forms.BooleanField( + label=_('Show multiple choice answers grouped in one column'), + initial=False, + required=False + )), + ('date_from', + forms.DateField( + label=_('Start date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include orders created on or after this date.') + )), + ('date_to', + forms.DateField( + label=_('End date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include orders issued on or before this date.') + )), + ('event_date_from', + forms.DateField( + label=_('Start event date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include orders including at least one ticket for a date on or after this date. ' + 'Will also include other dates in case of mixed orders!') + )), + ('event_date_to', + forms.DateField( + label=_('End event date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include orders including at least one ticket for a date on or after this date. ' + 'Will also include other dates in case of mixed orders!') + )), + ] + d = OrderedDict(d) + if not self.is_multievent and not self.event.has_subevents: + del d['event_date_from'] + del d['event_date_to'] + return d def _get_all_payment_methods(self, qs): pps = dict(get_all_payment_providers()) @@ -107,6 +141,51 @@ class OrderListExporter(MultiSheetListExporter): def event_object_cache(self): return {e.pk: e for e in self.events} + def _date_filter(self, qs, form_data, rel): + annotations = {} + filters = {} + + if form_data.get('date_from'): + date_value = form_data.get('date_from') + if isinstance(date_value, str): + date_value = dateutil.parser.parse(date_value).date() + + annotations['date'] = TruncDate(f'{rel}datetime') + filters['date__gte'] = date_value + + if form_data.get('date_to'): + date_value = form_data.get('date_to') + if isinstance(date_value, str): + date_value = dateutil.parser.parse(date_value).date() + + annotations['date'] = TruncDate(f'{rel}datetime') + filters['date__lte'] = date_value + + if form_data.get('event_date_from'): + date_value = form_data.get('event_date_from') + if isinstance(date_value, str): + date_value = dateutil.parser.parse(date_value).date() + + annotations['event_date_max'] = Case( + When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')), + default=F(f'{rel}event__date_from'), + ) + filters['event_date_max__gte'] = date_value + + if form_data.get('event_date_to'): + date_value = form_data.get('event_date_to') + if isinstance(date_value, str): + date_value = dateutil.parser.parse(date_value).date() + + annotations['event_date_min'] = Case( + When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')), + default=F(f'{rel}event__date_from'), + ) + filters['event_date_min__lte'] = date_value + + if filters: + return qs.annotate(**annotations).filter(**filters) + def iterate_orders(self, form_data: dict): p_date = OrderPayment.objects.filter( order=OuterRef('pk'), @@ -143,6 +222,9 @@ class OrderListExporter(MultiSheetListExporter): invoice_numbers=Subquery(i_numbers, output_field=CharField()), pcnt=Subquery(s, output_field=IntegerField()) ).select_related('invoice_address') + + qs = self._date_filter(qs, form_data, rel='') + if form_data['paid_only']: qs = qs.filter(status=Order.STATUS_PAID) tax_rates = self._get_all_tax_rates(qs) @@ -308,6 +390,8 @@ class OrderListExporter(MultiSheetListExporter): if form_data['paid_only']: qs = qs.filter(order__status=Order.STATUS_PAID) + qs = self._date_filter(qs, form_data, rel='order__') + headers = [ _('Event slug'), _('Order code'), @@ -406,6 +490,8 @@ class OrderListExporter(MultiSheetListExporter): if form_data['paid_only']: qs = qs.filter(order__status=Order.STATUS_PAID) + qs = self._date_filter(qs, form_data, rel='order__') + has_subevents = self.events.filter(has_subevents=True).exists() headers = [ diff --git a/src/pretix/plugins/badges/exporters.py b/src/pretix/plugins/badges/exporters.py index 82164d947..be3042cbc 100644 --- a/src/pretix/plugins/badges/exporters.py +++ b/src/pretix/plugins/badges/exporters.py @@ -1,16 +1,19 @@ import copy import json from collections import OrderedDict +from datetime import datetime, time, timedelta from io import BytesIO from typing import Tuple +import dateutil.parser from django import forms from django.conf import settings from django.contrib.staticfiles import finders from django.core.files import File from django.core.files.storage import default_storage -from django.db.models import Exists, OuterRef +from django.db.models import Exists, OuterRef, Q from django.db.models.functions import Coalesce +from django.utils.timezone import make_aware from django.utils.translation import gettext as _, gettext_lazy from jsonfallback.functions import JSONExtract from reportlab.lib import pagesizes @@ -236,12 +239,27 @@ class BadgeExporter(BaseExporter): 'want to print to a sheet of stickers with a regular office printer. Please note ' 'that your individual badge layouts must already be in the correct size.') )), + ('date_from', + forms.DateField( + label=_('Start date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include tickets for dates on or after this date.') + )), + ('date_to', + forms.DateField( + label=_('End date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include tickets for dates on or before this date.') + )), ('order_by', forms.ChoiceField( label=_('Sort by'), choices=[ ('name', _('Attendee name')), ('code', _('Order code')), + ('date', _('Event date')), ] + ([ ('name:{}'.format(k), _('Attendee name: {part}').format(part=label)) for k, label, w in name_scheme['fields'] @@ -266,10 +284,26 @@ class BadgeExporter(BaseExporter): else: qs = qs.filter(order__status__in=[Order.STATUS_PAID]) + if form_data.get('date_from'): + dt = make_aware(datetime.combine( + dateutil.parser.parse(form_data['date_from']).date(), + time(hour=0, minute=0, second=0) + ), self.event.timezone) + qs = qs.filter(Q(subevent__date_from__gte=dt) | Q(subevent__isnull=True, order__event__date_from__gte=dt)) + + if form_data.get('date_to'): + dt = make_aware(datetime.combine( + dateutil.parser.parse(form_data['date_to']).date() + timedelta(days=1), + time(hour=0, minute=0, second=0) + ), self.event.timezone) + qs = qs.filter(Q(subevent__date_from__lt=dt) | Q(subevent__isnull=True, order__event__date_from__lt=dt)) + if form_data.get('order_by') == 'name': qs = qs.order_by('attendee_name_cached', 'order__code') elif form_data.get('order_by') == 'code': qs = qs.order_by('order__code') + elif form_data.get('order_by') == 'date': + qs = qs.annotate(ed=Coalesce('subevent__date_from', 'order__event__date_from')).order_by('ed', 'order__code') elif form_data.get('order_by', '').startswith('name:'): part = form_data['order_by'][5:] qs = qs.annotate( diff --git a/src/pretix/plugins/ticketoutputpdf/exporters.py b/src/pretix/plugins/ticketoutputpdf/exporters.py index 5217adf08..6f70de65c 100644 --- a/src/pretix/plugins/ticketoutputpdf/exporters.py +++ b/src/pretix/plugins/ticketoutputpdf/exporters.py @@ -1,10 +1,14 @@ from collections import OrderedDict +from datetime import datetime, time, timedelta from io import BytesIO +import dateutil.parser from django import forms from django.conf import settings from django.core.files.base import ContentFile +from django.db.models import Q from django.db.models.functions import Coalesce +from django.utils.timezone import make_aware from django.utils.translation import gettext as _, gettext_lazy from jsonfallback.functions import JSONExtract from PyPDF2.merger import PdfFileMerger @@ -32,12 +36,27 @@ class AllTicketsPDF(BaseExporter): label=_('Include pending orders'), required=False )), + ('date_from', + forms.DateField( + label=_('Start date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include tickets for dates on or after this date.') + )), + ('date_to', + forms.DateField( + label=_('End date'), + widget=forms.DateInput(attrs={'class': 'datepickerfield'}), + required=False, + help_text=_('Only include tickets for dates on or before this date.') + )), ('order_by', forms.ChoiceField( label=_('Sort by'), choices=[ ('name', _('Attendee name')), ('code', _('Order code')), + ('date', _('Event date')), ] + ([ ('name:{}'.format(k), _('Attendee name: {part}').format(part=label)) for k, label, w in name_scheme['fields'] @@ -45,6 +64,11 @@ class AllTicketsPDF(BaseExporter): )), ] ) + + if not self.is_multievent and not self.event.has_subevents: + del d['date_from'] + del d['date_to'] + return d def render(self, form_data): @@ -60,10 +84,26 @@ class AllTicketsPDF(BaseExporter): else: qs = qs.filter(order__status__in=[Order.STATUS_PAID]) + if form_data.get('date_from'): + dt = make_aware(datetime.combine( + dateutil.parser.parse(form_data['date_from']).date(), + time(hour=0, minute=0, second=0) + ), self.event.timezone) + qs = qs.filter(Q(subevent__date_from__gte=dt) | Q(subevent__isnull=True, order__event__date_from__gte=dt)) + + if form_data.get('date_to'): + dt = make_aware(datetime.combine( + dateutil.parser.parse(form_data['date_to']).date() + timedelta(days=1), + time(hour=0, minute=0, second=0) + ), self.event.timezone) + qs = qs.filter(Q(subevent__date_from__lt=dt) | Q(subevent__isnull=True, order__event__date_from__lt=dt)) + if form_data.get('order_by') == 'name': qs = qs.order_by('attendee_name_cached', 'order__code') elif form_data.get('order_by') == 'code': qs = qs.order_by('order__code') + elif form_data.get('order_by') == 'date': + qs = qs.annotate(ed=Coalesce('subevent__date_from', 'order__event__date_from')).order_by('ed', 'order__code') elif form_data.get('order_by', '').startswith('name:'): part = form_data['order_by'][5:] qs = qs.annotate( diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 5863052a0..d22560c2e 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -23,7 +23,9 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView from pretix.base.channels import get_all_sales_channels -from pretix.base.models import ItemVariation, Quota, SeatCategoryMapping, Voucher +from pretix.base.models import ( + ItemVariation, Quota, SeatCategoryMapping, Voucher, +) from pretix.base.models.event import SubEvent from pretix.base.models.items import ( ItemBundle, SubEventItem, SubEventItemVariation, diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py index 6ecac8583..93379b019 100644 --- a/src/pretix/presale/views/organizer.py +++ b/src/pretix/presale/views/organizer.py @@ -19,7 +19,7 @@ from pytz import UTC from pretix.base.i18n import language from pretix.base.models import ( - Event, EventMetaValue, SubEvent, SubEventMetaValue, Quota, + Event, EventMetaValue, Quota, SubEvent, SubEventMetaValue, ) from pretix.base.services.quotas import QuotaAvailability from pretix.helpers.compat import date_fromisocalendar