diff --git a/src/pretix/base/services/stats.py b/src/pretix/base/services/stats.py index 0b5862e45d..22a1df5e25 100644 --- a/src/pretix/base/services/stats.py +++ b/src/pretix/base/services/stats.py @@ -1,12 +1,16 @@ +from datetime import date, datetime, time, timedelta from decimal import Decimal from typing import Any, Dict, Iterable, List, Tuple -from django.db.models import Case, Count, F, Sum, Value, When +from django.db.models import ( + Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When, +) +from django.utils.timezone import make_aware from django.utils.translation import ugettext_lazy as _ from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition from pretix.base.models.event import SubEvent -from pretix.base.models.orders import OrderFee +from pretix.base.models.orders import OrderFee, OrderPayment from pretix.base.signals import order_fee_type_name @@ -71,8 +75,9 @@ def dictsum(*dicts) -> dict: return res -def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[ItemCategory, List[Item]]], - Dict[str, Tuple[Decimal, Decimal]]]: +def order_overview( + event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None +) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]: items = event.items.all().select_related( 'category', # for re-grouping ).prefetch_related( @@ -82,6 +87,38 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It qs = OrderPosition.all if subevent: qs = qs.filter(subevent=subevent) + + if date_from and isinstance(date_from, date): + date_from = make_aware(datetime.combine( + date_from, + time(hour=0, minute=0, second=0, microsecond=0) + ), event.timezone) + + if date_until and isinstance(date_until, date): + date_until = make_aware(datetime.combine( + date_until + timedelta(days=1), + time(hour=0, minute=0, second=0, microsecond=0) + ), event.timezone) + + if date_filter == 'order_date': + if date_from: + qs = qs.filter(order__datetime__gte=date_from) + if date_until: + qs = qs.filter(order__datetime__lt=date_until) + elif date_filter == 'last_payment_date': + p_date = OrderPayment.objects.filter( + order=OuterRef('order'), + state__in=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED], + payment_date__isnull=False + ).values('order').annotate( + m=Max('payment_date') + ).values('m').order_by() + qs = qs.annotate(payment_date=Subquery(p_date, output_field=DateTimeField())) + if date_from: + qs = qs.filter(payment_date__gte=date_from) + if date_until: + qs = qs.filter(payment_date__lt=date_until) + counters = qs.filter( order__event=event ).annotate( @@ -153,14 +190,26 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It payment_items = [] if not subevent: - counters = OrderFee.all.filter( + qs = OrderFee.all.filter( order__event=event ).annotate( status=Case( When(canceled=True, then=Value('c')), default=F('order__status') ) - ).values( + ) + if date_filter == 'order_date': + if date_from: + qs = qs.filter(order__datetime__gte=date_from) + if date_until: + qs = qs.filter(order__datetime__lt=date_until) + elif date_filter == 'last_payment_date': + qs = qs.annotate(payment_date=Subquery(p_date, output_field=DateTimeField())) + if date_from: + qs = qs.filter(payment_date__gte=date_from) + if date_until: + qs = qs.filter(payment_date__lt=date_until) + counters = qs.values( 'fee_type', 'internal_type', 'status' ).annotate(cnt=Count('id'), value=Sum('value'), tax_value=Sum('tax_value')).order_by() diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 5d4b2aad19..c92f744eec 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -940,3 +940,51 @@ class RefundFilterForm(FilterForm): OrderRefund.REFUND_STATE_EXTERNAL]) return qs + + +class OverviewFilterForm(FilterForm): + subevent = forms.ModelChoiceField( + label=pgettext_lazy('subevent', 'Date'), + queryset=SubEvent.objects.none(), + required=False, + empty_label=pgettext_lazy('subevent', 'All dates') + ) + date_axis = forms.ChoiceField( + label=_('Date filter'), + choices=( + ('', _('Filter by…')), + ('order_date', _('Order date')), + ('last_payment_date', _('Date of last successful payment')), + ), + required=False, + ) + date_from = forms.DateField( + label=_('Date from'), + required=False, + widget=DatePickerWidget, + ) + date_until = forms.DateField( + label=_('Date until'), + required=False, + widget=DatePickerWidget, + ) + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event') + super().__init__(*args, **kwargs) + + if self.event.has_subevents: + self.fields['subevent'].queryset = self.event.subevents.all() + self.fields['subevent'].widget = Select2( + attrs={ + 'data-model-select2': 'event', + 'data-select2-url': reverse('control:event.subevents.select2', kwargs={ + 'event': self.event.slug, + 'organizer': self.event.organizer.slug, + }), + 'data-placeholder': pgettext_lazy('subevent', 'All dates') + } + ) + self.fields['subevent'].widget.choices = self.fields['subevent'].choices + elif 'subevent': + del self.fields['subevent'] diff --git a/src/pretix/control/templates/pretixcontrol/orders/overview.html b/src/pretix/control/templates/pretixcontrol/orders/overview.html index bfe87b89fc..0be85e3867 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/overview.html +++ b/src/pretix/control/templates/pretixcontrol/orders/overview.html @@ -1,5 +1,6 @@ {% extends "pretixcontrol/event/base.html" %} {% load i18n %} +{% load bootstrap3 %} {% load order_overview %} {% block title %}{% trans "Order overview" %}{% endblock %} {% block content %} @@ -12,11 +13,36 @@

{% trans "Order overview" %}

- {% if request.event.has_subevents %} -
- {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} +
+ + {% if request.event.has_subevents %} +
+ {% bootstrap_field filter_form.subevent layout='inline' %} +
+
+ {% bootstrap_field filter_form.date_axis layout='inline' %} +
+ {% else %} +
+ {% bootstrap_field filter_form.date_axis layout='inline' %} +
+ {% endif %} +
+ {% bootstrap_field filter_form.date_from layout='inline' %} +
+
+ {% bootstrap_field filter_form.date_until layout='inline' %} +
+
+ +
- {% endif %} +
{% if subevent_warning %}
{% blocktrans trimmed context "subevent" %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 16e25fa9ef..08db73ebe8 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -38,7 +38,6 @@ from pretix.base.models import ( Item, ItemVariation, LogEntry, Order, QuestionAnswer, Quota, generate_position_secret, generate_secret, ) -from pretix.base.models.event import SubEvent from pretix.base.models.orders import ( OrderFee, OrderPayment, OrderPosition, OrderRefund, ) @@ -66,7 +65,9 @@ from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.tasks import AsyncAction -from pretix.control.forms.filter import EventOrderFilterForm, RefundFilterForm +from pretix.control.forms.filter import ( + EventOrderFilterForm, OverviewFilterForm, RefundFilterForm, +) from pretix.control.forms.orders import ( CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm, @@ -1593,21 +1594,32 @@ class OverView(EventPermissionRequiredMixin, TemplateView): template_name = 'pretixcontrol/orders/overview.html' permission = 'can_view_orders' + @cached_property + def filter_form(self): + return OverviewFilterForm(data=self.request.GET, event=self.request.event) + def get_context_data(self, **kwargs): ctx = super().get_context_data() - subevent = None - if self.request.GET.get("subevent", "") != "" and self.request.event.has_subevents: - i = self.request.GET.get("subevent", "") - try: - subevent = self.request.event.subevents.get(pk=i) - except SubEvent.DoesNotExist: - pass - - ctx['items_by_category'], ctx['total'] = order_overview(self.request.event, subevent=subevent) - ctx['subevent_warning'] = self.request.event.has_subevents and subevent and ( + if self.filter_form.is_valid(): + ctx['items_by_category'], ctx['total'] = order_overview( + self.request.event, + subevent=self.filter_form.cleaned_data.get('subevent'), + date_filter=self.filter_form.cleaned_data['date_axis'], + date_from=self.filter_form.cleaned_data['date_from'], + date_until=self.filter_form.cleaned_data['date_until'], + ) + else: + ctx['items_by_category'], ctx['total'] = order_overview( + self.request.event, + ) + ctx['subevent_warning'] = ( + self.request.event.has_subevents and + self.filter_form.is_valid() and + self.filter_form.cleaned_data.get('subevent') and OrderFee.objects.filter(order__event=self.request.event).exclude(value=0).exists() ) + ctx['filter_form'] = self.filter_form return ctx diff --git a/src/pretix/plugins/reports/exporters.py b/src/pretix/plugins/reports/exporters.py index 2a255e3e87..64029d4d1f 100644 --- a/src/pretix/plugins/reports/exporters.py +++ b/src/pretix/plugins/reports/exporters.py @@ -3,6 +3,7 @@ from collections import OrderedDict, defaultdict from decimal import Decimal import pytz +from dateutil.parser import parse from django import forms from django.conf import settings from django.contrib.staticfiles import finders @@ -11,7 +12,7 @@ from django.db.models import Max, OuterRef, Subquery, Sum from django.template.defaultfilters import floatformat from django.utils.formats import date_format, localize from django.utils.timezone import get_current_timezone, now -from django.utils.translation import pgettext, pgettext_lazy, ugettext as _ +from django.utils.translation import pgettext, ugettext as _ from reportlab.lib import colors from pretix.base.decimal import round_decimal @@ -20,6 +21,7 @@ from pretix.base.models import Order, OrderPosition from pretix.base.models.event import SubEvent from pretix.base.models.orders import OrderFee, OrderPayment from pretix.base.services.stats import order_overview +from pretix.control.forms.filter import OverviewFilterForm class ReportlabExportMixin: @@ -160,6 +162,11 @@ class OverviewReport(Report): from reportlab.platypus import Paragraph, Spacer, TableStyle, Table from reportlab.lib.units import mm + if form_data.get('date_from'): + form_data['date_from'] = parse(form_data['date_from']) + if form_data.get('date_until'): + form_data['date_until'] = parse(form_data['date_until']) + headlinestyle = self.get_style() headlinestyle.fontSize = 15 headlinestyle.fontName = 'OpenSansBd' @@ -190,7 +197,17 @@ class OverviewReport(Report): Paragraph(_('Orders by product'), headlinestyle), Spacer(1, 5 * mm) ] - if self.form_data.get('subevent'): + if form_data.get('date_axis'): + story += [ + Paragraph(_('{axis} between {start} and {end}').format( + axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')], + start=date_format(form_data.get('date_from'), 'SHORT_DATE_FORMAT') if form_data.get('date_from') else '–', + end=date_format(form_data.get('date_until'), 'SHORT_DATE_FORMAT') if form_data.get('date_until') else '–', + ), self.get_style()), + Spacer(1, 5 * mm) + ] + + if form_data.get('subevent'): try: subevent = self.event.subevents.get(pk=self.form_data.get('subevent')) except SubEvent.DoesNotExist: @@ -215,7 +232,13 @@ class OverviewReport(Report): ], ] - items_by_category, total = order_overview(self.event, subevent=self.form_data.get('subevent')) + items_by_category, total = order_overview( + self.event, + subevent=form_data.get('subevent'), + date_filter=form_data.get('date_axis'), + date_from=form_data.get('date_from'), + date_until=form_data.get('date_until'), + ) places = settings.CURRENCY_PLACES.get(self.event.currency, 2) states = ( ('canceled', Order.STATUS_CANCELED), @@ -264,15 +287,9 @@ class OverviewReport(Report): @property def export_form_fields(self) -> dict: - d = OrderedDict() - if self.event.has_subevents: - d['subevent'] = forms.ModelChoiceField( - self.event.subevents.all(), - label=pgettext_lazy('subevent', 'Date'), - required=False, - empty_label=pgettext_lazy('subevent', 'All dates') - ) - return d + f = OverviewFilterForm(event=self.event) + del f.fields['ordering'] + return f.fields class OrderTaxListReport(Report):