Order overview: Allow to filter by date

This commit is contained in:
Raphael Michel
2019-07-07 19:45:22 +02:00
parent 42af8b1602
commit 197ec84f05
5 changed files with 186 additions and 34 deletions

View File

@@ -1,12 +1,16 @@
from datetime import date, datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, Iterable, List, Tuple 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 django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
from pretix.base.models.event import SubEvent 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 from pretix.base.signals import order_fee_type_name
@@ -71,8 +75,9 @@ def dictsum(*dicts) -> dict:
return res return res
def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[ItemCategory, List[Item]]], def order_overview(
Dict[str, Tuple[Decimal, Decimal]]]: 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( items = event.items.all().select_related(
'category', # for re-grouping 'category', # for re-grouping
).prefetch_related( ).prefetch_related(
@@ -82,6 +87,38 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
qs = OrderPosition.all qs = OrderPosition.all
if subevent: if subevent:
qs = qs.filter(subevent=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( counters = qs.filter(
order__event=event order__event=event
).annotate( ).annotate(
@@ -153,14 +190,26 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
payment_items = [] payment_items = []
if not subevent: if not subevent:
counters = OrderFee.all.filter( qs = OrderFee.all.filter(
order__event=event order__event=event
).annotate( ).annotate(
status=Case( status=Case(
When(canceled=True, then=Value('c')), When(canceled=True, then=Value('c')),
default=F('order__status') 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' 'fee_type', 'internal_type', 'status'
).annotate(cnt=Count('id'), value=Sum('value'), tax_value=Sum('tax_value')).order_by() ).annotate(cnt=Count('id'), value=Sum('value'), tax_value=Sum('tax_value')).order_by()

View File

@@ -940,3 +940,51 @@ class RefundFilterForm(FilterForm):
OrderRefund.REFUND_STATE_EXTERNAL]) OrderRefund.REFUND_STATE_EXTERNAL])
return qs 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']

View File

@@ -1,5 +1,6 @@
{% extends "pretixcontrol/event/base.html" %} {% extends "pretixcontrol/event/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %}
{% load order_overview %} {% load order_overview %}
{% block title %}{% trans "Order overview" %}{% endblock %} {% block title %}{% trans "Order overview" %}{% endblock %}
{% block content %} {% block content %}
@@ -12,11 +13,36 @@
</div> </div>
</div> </div>
<h1>{% trans "Order overview" %}</h1> <h1>{% trans "Order overview" %}</h1>
{% if request.event.has_subevents %} <div class="row filter-form">
<form class="form-inline helper-display-inline" action="" method="get"> <form class="" action="" method="get">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} {% if request.event.has_subevents %}
<div class="col-lg-2 col-sm-3 col-xs-6">
{% bootstrap_field filter_form.subevent layout='inline' %}
</div>
<div class="col-lg-2 col-sm-3 col-xs-6">
{% bootstrap_field filter_form.date_axis layout='inline' %}
</div>
{% else %}
<div class="col-lg-4 col-sm-6 col-xs-6">
{% bootstrap_field filter_form.date_axis layout='inline' %}
</div>
{% endif %}
<div class="col-lg-2 col-sm-6 col-xs-6">
{% bootstrap_field filter_form.date_from layout='inline' %}
</div>
<div class="col-lg-2 col-sm-6 col-xs-6">
{% bootstrap_field filter_form.date_until layout='inline' %}
</div>
<div class="col-lg-1 col-lg-offset-3 col-sm-6 col-xs-6">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
<span class="hidden-md">
{% trans "Filter" %}
</span>
</button>
</div>
</form> </form>
{% endif %} </div>
{% if subevent_warning %} {% if subevent_warning %}
<div class="alert alert-info"> <div class="alert alert-info">
{% blocktrans trimmed context "subevent" %} {% blocktrans trimmed context "subevent" %}

View File

@@ -38,7 +38,6 @@ from pretix.base.models import (
Item, ItemVariation, LogEntry, Order, QuestionAnswer, Quota, Item, ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
generate_position_secret, generate_secret, generate_position_secret, generate_secret,
) )
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import ( from pretix.base.models.orders import (
OrderFee, OrderPayment, OrderPosition, OrderRefund, 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.templatetags.rich_text import markdown_compile_email
from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction 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 ( from pretix.control.forms.orders import (
CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm, CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm,
MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm, MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm,
@@ -1593,21 +1594,32 @@ class OverView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/overview.html' template_name = 'pretixcontrol/orders/overview.html'
permission = 'can_view_orders' 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data() ctx = super().get_context_data()
subevent = None if self.filter_form.is_valid():
if self.request.GET.get("subevent", "") != "" and self.request.event.has_subevents: ctx['items_by_category'], ctx['total'] = order_overview(
i = self.request.GET.get("subevent", "") self.request.event,
try: subevent=self.filter_form.cleaned_data.get('subevent'),
subevent = self.request.event.subevents.get(pk=i) date_filter=self.filter_form.cleaned_data['date_axis'],
except SubEvent.DoesNotExist: date_from=self.filter_form.cleaned_data['date_from'],
pass date_until=self.filter_form.cleaned_data['date_until'],
)
ctx['items_by_category'], ctx['total'] = order_overview(self.request.event, subevent=subevent) else:
ctx['subevent_warning'] = self.request.event.has_subevents and subevent and ( 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() OrderFee.objects.filter(order__event=self.request.event).exclude(value=0).exists()
) )
ctx['filter_form'] = self.filter_form
return ctx return ctx

View File

@@ -3,6 +3,7 @@ from collections import OrderedDict, defaultdict
from decimal import Decimal from decimal import Decimal
import pytz import pytz
from dateutil.parser import parse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders 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.template.defaultfilters import floatformat
from django.utils.formats import date_format, localize from django.utils.formats import date_format, localize
from django.utils.timezone import get_current_timezone, now 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 reportlab.lib import colors
from pretix.base.decimal import round_decimal 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.event import SubEvent
from pretix.base.models.orders import OrderFee, OrderPayment from pretix.base.models.orders import OrderFee, OrderPayment
from pretix.base.services.stats import order_overview from pretix.base.services.stats import order_overview
from pretix.control.forms.filter import OverviewFilterForm
class ReportlabExportMixin: class ReportlabExportMixin:
@@ -160,6 +162,11 @@ class OverviewReport(Report):
from reportlab.platypus import Paragraph, Spacer, TableStyle, Table from reportlab.platypus import Paragraph, Spacer, TableStyle, Table
from reportlab.lib.units import mm 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 = self.get_style()
headlinestyle.fontSize = 15 headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd' headlinestyle.fontName = 'OpenSansBd'
@@ -190,7 +197,17 @@ class OverviewReport(Report):
Paragraph(_('Orders by product'), headlinestyle), Paragraph(_('Orders by product'), headlinestyle),
Spacer(1, 5 * mm) 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: try:
subevent = self.event.subevents.get(pk=self.form_data.get('subevent')) subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
except SubEvent.DoesNotExist: 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) places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
states = ( states = (
('canceled', Order.STATUS_CANCELED), ('canceled', Order.STATUS_CANCELED),
@@ -264,15 +287,9 @@ class OverviewReport(Report):
@property @property
def export_form_fields(self) -> dict: def export_form_fields(self) -> dict:
d = OrderedDict() f = OverviewFilterForm(event=self.event)
if self.event.has_subevents: del f.fields['ordering']
d['subevent'] = forms.ModelChoiceField( return f.fields
self.event.subevents.all(),
label=pgettext_lazy('subevent', 'Date'),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
return d
class OrderTaxListReport(Report): class OrderTaxListReport(Report):