forked from CGM_Public/pretix_original
Order overview: Allow to filter by date
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
<h1>{% trans "Order overview" %}</h1>
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
<div class="row filter-form">
|
||||
<form class="" action="" method="get">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if subevent_warning %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed context "subevent" %}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user