Exports: Add predefined timeframes (#3027)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-01-13 13:14:08 +01:00
committed by GitHub
co-authored by Richard Schreiber
parent 95979143d7
commit bf4569b080
13 changed files with 715 additions and 269 deletions
+3
View File
@@ -192,6 +192,9 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"`` File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
specifiers in requests specifiers in requests
(see below). (see below).
Date range *either* two dates separated ``2022-03-18/2022-03-23``, ``2022-03-18/``,
by ``/`` *or* the name of a ``/2022-03-23``, ``week_this``, ``week_next``,
defined range. ``month_this``
===================== ============================ =================================== ===================== ============================ ===================================
Query parameters Query parameters
+14 -27
View File
@@ -23,10 +23,9 @@ import json
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
from pretix.base.i18n import language from pretix.base.i18n import language
@@ -34,6 +33,7 @@ from pretix.base.models import Invoice, OrderPayment
from ..exporter import BaseExporter from ..exporter import BaseExporter
from ..signals import register_data_exporters from ..signals import register_data_exporters
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
class DekodiNREIExporter(BaseExporter): class DekodiNREIExporter(BaseExporter):
@@ -115,7 +115,7 @@ class DekodiNREIExporter(BaseExporter):
'PTNo14': p.info_data.get('reference') or '', 'PTNo14': p.info_data.get('reference') or '',
'PTNo15': p.full_id or '', 'PTNo15': p.full_id or '',
}) })
elif p.provider.startswith('stripe'): elif p.provider and p.provider.startswith('stripe'):
src = p.info_data.get("source", p.info_data) src = p.info_data.get("source", p.info_data)
payments.append({ payments.append({
'PTID': '81', 'PTID': '81',
@@ -194,17 +194,12 @@ class DekodiNREIExporter(BaseExporter):
def render(self, form_data): def render(self, form_data):
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent') qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
if form_data.get('date_from'): if form_data.get('date_range'):
date_value = form_data.get('date_from') d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
if isinstance(date_value, str): if d_start:
date_value = dateutil.parser.parse(date_value).date() qs = qs.filter(date__gte=d_start)
qs = qs.filter(date__gte=date_value) if d_end:
qs = qs.filter(date__lte=d_end)
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()
qs = qs.filter(date__lte=date_value)
jo = { jo = {
'Format': 'NREI', 'Format': 'NREI',
@@ -220,22 +215,14 @@ class DekodiNREIExporter(BaseExporter):
def export_form_fields(self): def export_form_fields(self):
return OrderedDict( return OrderedDict(
[ [
('date_from', ('date_range',
forms.DateField( DateFrameField(
label=gettext_lazy('Start date'), label=gettext_lazy('Date range'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=False,
required=False, required=False,
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does ' help_text=gettext_lazy('Only include invoices issued in this time frame. Note that the invoice date does '
'not always correspond to the order or payment date.') 'not always correspond to the order or payment date.')
)), )),
('date_to',
forms.DateField(
label=gettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
] ]
) )
+13 -24
View File
@@ -38,12 +38,12 @@ from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
from zipfile import ZipFile from zipfile import ZipFile
import dateutil.parser
from django import forms from django import forms
from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ( from django.utils.translation import (
gettext, gettext_lazy as _, pgettext, pgettext_lazy, gettext, gettext_lazy as _, pgettext, pgettext_lazy,
) )
@@ -59,6 +59,7 @@ from ..services.invoices import invoice_pdf_task
from ..signals import ( from ..signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
) )
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
class InvoiceExporterMixin: class InvoiceExporterMixin:
@@ -68,22 +69,14 @@ class InvoiceExporterMixin:
def invoice_exporter_form_fields(self): def invoice_exporter_form_fields(self):
return OrderedDict( return OrderedDict(
[ [
('date_from', ('date_range',
forms.DateField( DateFrameField(
label=_('Start date'), label=_('Date range'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=False,
required=False, required=False,
help_text=_('Only include invoices issued on or after this date. Note that the invoice date does ' help_text=_('Only include invoices issued in this time frame. Note that the invoice date does '
'not always correspond to the order or payment date.') 'not always correspond to the order or payment date.')
)), )),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
('payment_provider', ('payment_provider',
forms.ChoiceField( forms.ChoiceField(
label=_('Payment provider'), label=_('Payment provider'),
@@ -115,16 +108,12 @@ class InvoiceExporterMixin:
) )
) )
qs = qs.filter(has_payment_with_provider=1) qs = qs.filter(has_payment_with_provider=1)
if form_data.get('date_from'): if form_data.get('date_range'):
date_value = form_data.get('date_from') d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
if isinstance(date_value, str): if d_start:
date_value = dateutil.parser.parse(date_value).date() qs = qs.filter(date__gte=d_start)
qs = qs.filter(date__gte=date_value) if d_end:
if form_data.get('date_to'): qs = qs.filter(date__lte=d_end)
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
return qs return qs
+55 -101
View File
@@ -33,10 +33,8 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict from collections import OrderedDict
from datetime import date, datetime, time
from decimal import Decimal from decimal import Decimal
import dateutil
import pytz import pytz
from django import forms from django import forms
from django.db.models import ( from django.db.models import (
@@ -46,7 +44,7 @@ from django.db.models import (
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now from django.utils.timezone import get_current_timezone, now
from django.utils.translation import ( from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy, gettext as _, gettext_lazy, pgettext, pgettext_lazy,
) )
@@ -68,6 +66,10 @@ from ..exporter import (
from ..signals import ( from ..signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
) )
from ..timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
class OrderListExporter(MultiSheetListExporter): class OrderListExporter(MultiSheetListExporter):
@@ -112,41 +114,25 @@ class OrderListExporter(MultiSheetListExporter):
initial=False, initial=False,
required=False required=False
)), )),
('date_from', ('date_range',
forms.DateField( DateFrameField(
label=_('Start date'), label=_('Date range'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=False,
required=False, required=False,
help_text=_('Only include orders created on or after this date.') help_text=_('Only include orders created within this date range.')
)), )),
('date_to', ('event_date_range',
forms.DateField( DateFrameField(
label=_('End date'), label=_('Event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=True,
required=False, required=False,
help_text=_('Only include orders created on or before this date.') help_text=_('Only include orders including at least one ticket for a date in this range. '
)),
('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 before this date. '
'Will also include other dates in case of mixed orders!') 'Will also include other dates in case of mixed orders!')
)), )),
] ]
d = OrderedDict(d) d = OrderedDict(d)
if not self.is_multievent and not self.event.has_subevents: if not self.is_multievent and not self.event.has_subevents:
del d['event_date_from'] del d['event_date_range']
del d['event_date_to']
return d return d
def _get_all_payment_methods(self, qs): def _get_all_payment_methods(self, qs):
@@ -189,45 +175,27 @@ class OrderListExporter(MultiSheetListExporter):
annotations = {} annotations = {}
filters = {} filters = {}
if form_data.get('date_from'): if form_data.get('date_range'):
date_value = form_data.get('date_from') dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if not isinstance(date_value, date): if dt_start:
date_value = dateutil.parser.parse(date_value).date() filters[f'{rel}datetime__gte'] = dt_start
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone) if dt_end:
filters[f'{rel}datetime__lt'] = dt_end
filters[f'{rel}datetime__gte'] = datetime_value if form_data.get('event_date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['event_date_range'], self.timezone)
if form_data.get('date_to'): if dt_start:
date_value = form_data.get('date_to') annotations['event_date_max'] = Case(
if not isinstance(date_value, date): When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
date_value = dateutil.parser.parse(date_value).date() default=F(f'{rel}event__date_from'),
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone) )
filters['event_date_max__gte'] = dt_start
filters[f'{rel}datetime__lte'] = datetime_value if dt_end:
annotations['event_date_min'] = Case(
if form_data.get('event_date_from'): When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
date_value = form_data.get('event_date_from') default=F(f'{rel}event__date_from'),
if not isinstance(date_value, date): )
date_value = dateutil.parser.parse(date_value).date() filters['event_date_min__lt'] = dt_end
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
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'] = datetime_value
if form_data.get('event_date_to'):
date_value = form_data.get('event_date_to')
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
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'] = datetime_value
if filters: if filters:
return qs.annotate(**annotations).filter(**filters) return qs.annotate(**annotations).filter(**filters)
@@ -926,17 +894,11 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
@property @property
def additional_form_fields(self): def additional_form_fields(self):
d = [ d = [
('date_from', ('date_range',
forms.DateField( DateFrameField(
label=_('Start date'), label=_('Date range'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=False,
required=False, required=False
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)), )),
] ]
d = OrderedDict(d) d = OrderedDict(d)
@@ -947,22 +909,12 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
card__issuer=self.organizer, card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event') ).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_from'): if form_data.get('date_range'):
date_value = form_data.get('date_from') dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if isinstance(date_value, str): if dt_start:
date_value = dateutil.parser.parse(date_value).date() qs = qs.filter(datetime__gte=dt_start)
qs = qs.filter( if dt_end:
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone) qs = qs.filter(datetime__lt=dt_end)
)
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()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [ headers = [
_('Gift card code'), _('Gift card code'),
@@ -1048,7 +1000,8 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
[ [
('date', forms.DateTimeField( ('date', forms.DateTimeField(
label=_('Show value at'), label=_('Show value at'),
initial=now(), required=False,
help_text=_('Defaults to the time of report.')
)), )),
('testmode', forms.ChoiceField( ('testmode', forms.ChoiceField(
label=_('Test mode'), label=_('Test mode'),
@@ -1076,12 +1029,13 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
) )
def iterate_list(self, form_data): def iterate_list(self, form_data):
d = form_data.get('date') or now()
s = GiftCardTransaction.objects.filter( s = GiftCardTransaction.objects.filter(
card=OuterRef('pk'), card=OuterRef('pk'),
datetime__lte=form_data['date'] datetime__lte=d
).order_by().values('card').annotate(s=Sum('value')).values('s') ).order_by().values('card').annotate(s=Sum('value')).values('s')
qs = self.organizer.issued_gift_cards.filter( qs = self.organizer.issued_gift_cards.filter(
issuance__lte=form_data['date'] issuance__lte=d
).annotate( ).annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00')), cached_value=Coalesce(Subquery(s), Decimal('0.00')),
).order_by('issuance').prefetch_related( ).order_by('issuance').prefetch_related(
@@ -1096,11 +1050,11 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
if form_data.get('state') == 'empty': if form_data.get('state') == 'empty':
qs = qs.filter(cached_value=0) qs = qs.filter(cached_value=0)
elif form_data.get('state') == 'valid_value': elif form_data.get('state') == 'valid_value':
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=form_data['date'])) qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=d))
elif form_data.get('state') == 'expired_value': elif form_data.get('state') == 'expired_value':
qs = qs.exclude(cached_value=0).filter(expires__lt=form_data['date']) qs = qs.exclude(cached_value=0).filter(expires__lt=d)
elif form_data.get('state') == 'expired': elif form_data.get('state') == 'expired':
qs = qs.filter(expires__lt=form_data['date']) qs = qs.filter(expires__lt=d)
headers = [ headers = [
_('Gift card code'), _('Gift card code'),
@@ -0,0 +1,18 @@
{% load i18n %}
{% with widget.subwidgets.0 as widget %}
{% include widget.template_name %}
{% endwith %}
<div class="row" data-display-dependency-value="custom" data-display-dependency="#{{ widget.subwidgets.0.attrs.id }}">
<br>
<div class="col-sm-6">
{% with widget.subwidgets.1 as widget %}
{% include widget.template_name %}
{% endwith %}
</div>
<div class="col-sm-6">
{% with widget.subwidgets.2 as widget %}
{% include widget.template_name %}
{% endwith %}
</div>
</div>
{% spaceless %}{% for widget in widget.subwidgets %}{% endfor %}{% endspaceless %}
+413
View File
@@ -0,0 +1,413 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import calendar
from datetime import date, datetime, time, timedelta
from itertools import groupby
from typing import Optional, Tuple
from django import forms
from django.core.exceptions import ValidationError
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy, pgettext_lazy
from pretix.helpers.daterange import daterange
def _quarter_start(ref_d):
return ref_d.replace(day=1, month=1 + (ref_d.month - 1) // 3 * 3)
def _week_start(ref_d):
return ref_d - timedelta(ref_d.weekday())
REPORTING_DATE_TIMEFRAMES = (
# (identifier, label, start_inclusive, end_inclusive, includes_future, optgroup, describe)
(
'days_today',
pgettext_lazy('reporting_timeframe', 'Today'),
lambda ref_d: ref_d,
lambda ref_d, start_d: start_d,
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_yesterday',
pgettext_lazy('reporting_timeframe', 'Yesterday'),
lambda ref_d: ref_d - timedelta(days=1),
lambda ref_d, start_d: start_d,
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_last7',
pgettext_lazy('reporting_timeframe', 'Last 7 days'),
lambda ref_d: ref_d - timedelta(days=6),
lambda ref_d, start_d: start_d + timedelta(days=6),
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_last14',
pgettext_lazy('reporting_timeframe', 'Last 14 days'),
lambda ref_d: ref_d - timedelta(days=13),
lambda ref_d, start_d: start_d + timedelta(days=13),
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_tomorrow',
pgettext_lazy('reporting_timeframe', 'Tomorrow'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d,
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_next7',
pgettext_lazy('reporting_timeframe', 'Next 7 days'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_next14',
pgettext_lazy('reporting_timeframe', 'Next 14 days'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d + timedelta(days=13),
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'week_this',
pgettext_lazy('reporting_timeframe', 'Current week'),
lambda ref_d: _week_start(ref_d),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_to_date',
pgettext_lazy('reporting_timeframe', 'Current week to date'),
lambda ref_d: _week_start(ref_d),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_previous',
pgettext_lazy('reporting_timeframe', 'Previous week'),
lambda ref_d: _week_start(ref_d) - timedelta(days=7),
lambda ref_d, start_d: start_d + timedelta(days=6),
False,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_next',
pgettext_lazy('reporting_timeframe', 'Next week'),
lambda ref_d: _week_start(ref_d + timedelta(days=7)),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'month_this',
pgettext_lazy('reporting_timeframe', 'Current month'),
lambda ref_d: ref_d.replace(day=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
True,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_to_date',
pgettext_lazy('reporting_timeframe', 'Current month to date'),
lambda ref_d: ref_d.replace(day=1),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_previous',
pgettext_lazy('reporting_timeframe', 'Previous month'),
lambda ref_d: (ref_d.replace(day=1) - timedelta(days=1)).replace(day=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
False,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_next',
pgettext_lazy('reporting_timeframe', 'Next month'),
lambda ref_d: ref_d.replace(day=calendar.monthrange(ref_d.year, ref_d.month)[1]) + timedelta(days=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
True,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'quarter_this',
pgettext_lazy('reporting_timeframe', 'Current quarter'),
lambda ref_d: _quarter_start(ref_d),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
True,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_to_date',
pgettext_lazy('reporting_timeframe', 'Current quarter to date'),
lambda ref_d: _quarter_start(ref_d),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_previous',
pgettext_lazy('reporting_timeframe', 'Previous quarter'),
lambda ref_d: _quarter_start(_quarter_start(ref_d) - timedelta(days=1)),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
False,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_next',
pgettext_lazy('reporting_timeframe', 'Next quarter'),
lambda ref_d: ref_d.replace(
day=calendar.monthrange(ref_d.year, _quarter_start(ref_d).month + 2)[1], month=_quarter_start(ref_d).month + 2
) + timedelta(days=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
True,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'year_this',
pgettext_lazy('reporting_timeframe', 'Current year'),
lambda ref_d: ref_d.replace(day=1, month=1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
True,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_to_date',
pgettext_lazy('reporting_timeframe', 'Current year to date'),
lambda ref_d: ref_d.replace(day=1, month=1),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_previous',
pgettext_lazy('reporting_timeframe', 'Previous year'),
lambda ref_d: (ref_d.replace(day=1, month=1) - timedelta(days=1)).replace(day=1, month=1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
False,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_next',
pgettext_lazy('reporting_timeframe', 'Next year'),
lambda ref_d: ref_d.replace(day=1, month=1, year=ref_d.year + 1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
True,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'future',
pgettext_lazy('reporting_timeframe', 'All future (excluding today)'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: None,
True,
pgettext_lazy('reporting_timeframe', 'Other'),
lambda start_d, end_d: date_format(start_d, "SHORT_DATE_FORMAT") + ' ',
),
(
'past',
pgettext_lazy('reporting_timeframe', 'All past (including today)'),
lambda ref_d: None,
lambda ref_d, start_d: ref_d,
True, # technically false, but only makes sense to have in a selection that also allows the future, otherwise redundant
pgettext_lazy('reporting_timeframe', 'Other'),
lambda start_d, end_d: ' ' + date_format(end_d, "SHORT_DATE_FORMAT"),
),
)
class DateFrameWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/dateframe.html'
def __init__(self, *args, **kwargs):
self.timeframe_choices = kwargs.pop('timeframe_choices')
widgets = (
forms.Select(choices=self.timeframe_choices),
forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'Start')}),
forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'End')}),
)
super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value):
if not value:
return ['unset', None, None]
if '/' in value:
return [
'custom',
date.fromisoformat(value.split('/', 1)[0]),
date.fromisoformat(value.split('/', 1)[-1]),
]
return []
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['required'] = self.timeframe_choices[0][0] == 'unset'
return ctx
def _describe_timeframe(label, start, end, future, describe):
d_start = start(now())
d_end = end(now(), d_start)
details = describe(d_start, d_end)
return f'{label} ({details})'
class DateFrameField(forms.MultiValueField):
default_error_messages = {
**forms.MultiValueField.default_error_messages,
'inconsistent': gettext_lazy('The end date must be after the start date.'),
}
def __init__(self, *args, **kwargs):
include_future_frames = kwargs.pop('include_future_frames')
top_choices = [('custom', gettext_lazy('Custom timeframe'))]
if not kwargs.get('required', True):
top_choices.insert(0, ('unset', pgettext_lazy('reporting_timeframe', 'All time')))
_choices = []
for grouper, group in groupby(REPORTING_DATE_TIMEFRAMES, key=lambda i: i[5]):
options = [
(identifier, _describe_timeframe(label, start, end, future, describe))
for identifier, label, start, end, future, group, describe in group
if include_future_frames or not future
]
if options:
_choices.append((grouper, options))
timeframe_choices = [
('', top_choices)
] + _choices
fields = (
forms.ChoiceField(
choices=timeframe_choices,
required=True
),
forms.DateField(
required=False
),
forms.DateField(
required=False
),
)
if 'widget' not in kwargs:
kwargs['widget'] = DateFrameWidget(timeframe_choices=timeframe_choices)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
def compress(self, data_list):
if not data_list:
return None
if data_list[0] == 'unset':
return None
elif data_list[0] == 'custom':
return f'{data_list[1].isoformat() if data_list[1] else ""}/{data_list[2].isoformat() if data_list[2] else ""}'
else:
return data_list[0]
def has_changed(self, initial, data):
if initial is None:
initial = self.widget.decompress(initial)
return super().has_changed(initial, data)
def clean(self, value):
if value[0] == 'custom':
if not value[1] and not value[2]:
raise ValidationError(self.error_messages['incomplete'])
if value[1] and value[2] and value[2] < value[1]:
raise ValidationError(self.error_messages['inconsistent'])
return super().clean(value)
def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of dates
where the first element ist the first possible date value within the timeframe and the second
element is the last possible date value in the timeframe.
Both returned values may be ``None`` for an unlimited interval.
"""
if isinstance(ref_dt, datetime):
ref_dt = ref_dt.astimezone(timezone).date()
if "/" in frame:
start, end = frame.split("/", 1)
return date.fromisoformat(start) if start else None, date.fromisoformat(end) if end else None
for idf, label, start, end, includes_future, *args in REPORTING_DATE_TIMEFRAMES:
if frame == idf:
d_start = start(ref_dt)
d_end = end(ref_dt, d_start)
return d_start, d_end
raise ValueError(f"Invalid timeframe '{frame}'")
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
where the first element ist the first possible datetime within the timeframe and the second
element is the first possible datetime value *not* in the timeframe.
Both returned values may be ``None`` for an unlimited interval.
"""
d_start, d_end = resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone)
dt_start = make_aware(datetime.combine(d_start, time(0, 0, 0)), timezone) if d_start else None
dt_end = make_aware(datetime.combine(d_end + timedelta(days=1), time(0, 0, 0)), timezone) if d_end else None
return dt_start, dt_end
+17 -29
View File
@@ -33,7 +33,6 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, time, timedelta
import bleach import bleach
import dateutil.parser import dateutil.parser
@@ -44,7 +43,7 @@ from django.db.models import (
from django.db.models.functions import Coalesce, NullIf from django.db.models.functions import Coalesce, NullIf
from django.urls import reverse from django.urls import reverse
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.timezone import is_aware, make_aware from django.utils.timezone import is_aware, make_aware, now
from django.utils.translation import ( from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy, gettext as _, gettext_lazy, pgettext, pgettext_lazy,
) )
@@ -58,6 +57,10 @@ from pretix.base.models import (
) )
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.base.timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.helpers.templatetags.jsonfield import JSONExtract from pretix.helpers.templatetags.jsonfield import JSONExtract
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -78,19 +81,12 @@ class CheckInListMixin(BaseExporter):
), ),
initial=self.event.checkin_lists.first() initial=self.event.checkin_lists.first()
)), )),
('date_from', ('date_range',
forms.DateField( DateFrameField(
label=_('Start date'), label=_('Date range'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=True,
required=False, required=False,
help_text=_('Only include tickets for dates on or after this date.') help_text=_('Only include tickets for dates within this range.')
)),
('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.')
)), )),
('secrets', ('secrets',
forms.BooleanField( forms.BooleanField(
@@ -129,8 +125,7 @@ class CheckInListMixin(BaseExporter):
) )
if not self.event.has_subevents: if not self.event.has_subevents:
del d['date_from'] del d['date_range']
del d['date_to']
d['list'].queryset = self.event.checkin_lists.all() d['list'].queryset = self.event.checkin_lists.all()
d['list'].widget = Select2( d['list'].widget = Select2(
@@ -181,19 +176,12 @@ class CheckInListMixin(BaseExporter):
if cl.subevent: if cl.subevent:
qs = qs.filter(subevent=cl.subevent) qs = qs.filter(subevent=cl.subevent)
if form_data.get('date_from'): if form_data.get('date_range'):
dt = make_aware(datetime.combine( dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
dateutil.parser.parse(form_data['date_from']).date(), if dt_start:
time(hour=0, minute=0, second=0) qs = qs.filter(subevent__date_from__gte=dt_start)
), self.event.timezone) if dt_end:
qs = qs.filter(subevent__date_from__gte=dt) qs = qs.filter(subevent__date_to__lt=dt_end)
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(subevent__date_from__lt=dt)
o = () o = ()
if self.event.has_subevents and not cl.subevent: if self.event.has_subevents and not cl.subevent:
+42 -46
View File
@@ -35,7 +35,6 @@
import copy import copy
import tempfile import tempfile
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from datetime import date, datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
import pytz import pytz
@@ -47,7 +46,7 @@ from django.db import models
from django.db.models import DateTimeField, Max, OuterRef, Subquery, Sum from django.db.models import DateTimeField, 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, make_aware, now from django.utils.timezone import get_current_timezone, now
from django.utils.translation import ( from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy, gettext as _, gettext_lazy, pgettext, pgettext_lazy,
) )
@@ -59,11 +58,14 @@ from reportlab.platypus import PageBreak, Paragraph, Spacer, Table, TableStyle
from pretix.base.decimal import round_decimal from pretix.base.decimal import round_decimal
from pretix.base.exporter import BaseExporter, MultiSheetListExporter from pretix.base.exporter import BaseExporter, MultiSheetListExporter
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import Order, OrderPosition 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.base.timeframes import (
DateFrameField, resolve_timeframe_to_dates_inclusive,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
from pretix.control.forms.filter import OverviewFilterForm from pretix.control.forms.filter import OverviewFilterForm
@@ -236,12 +238,13 @@ class OverviewReport(Report):
def _filter_story(self, doc, form_data, net=False): def _filter_story(self, doc, form_data, net=False):
story = [] story = []
if form_data.get('date_axis') and (form_data.get('date_from') or form_data.get('date_until')): if form_data.get('date_axis') and form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
story += [ story += [
Paragraph(_('{axis} between {start} and {end}').format( Paragraph(_('{axis} between {start} and {end}').format(
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')], 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 '', start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(form_data.get('date_until'), 'SHORT_DATE_FORMAT') if form_data.get('date_until') else '', end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '',
), self.get_style()), ), self.get_style()),
Spacer(1, 5 * mm) Spacer(1, 5 * mm)
] ]
@@ -256,12 +259,16 @@ class OverviewReport(Report):
return story return story
def _get_data(self, form_data): def _get_data(self, form_data):
if form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
else:
d_start, d_end = None, None
return order_overview( return order_overview(
self.event, self.event,
subevent=form_data.get('subevent'), subevent=form_data.get('subevent'),
date_filter=form_data.get('date_axis'), date_filter=form_data.get('date_axis'),
date_from=form_data.get('date_from'), date_from=d_start,
date_until=form_data.get('date_until'), date_until=d_end,
fees=True fees=True
) )
@@ -381,6 +388,13 @@ class OverviewReport(Report):
def export_form_fields(self) -> dict: def export_form_fields(self) -> dict:
f = OverviewFilterForm(event=self.event) f = OverviewFilterForm(event=self.event)
del f.fields['ordering'] del f.fields['ordering']
del f.fields['date_from']
del f.fields['date_until']
f.fields['date_range'] = DateFrameField(
label=_('Date range'),
include_future_frames=False,
required=False,
)
return f.fields return f.fields
@@ -606,48 +620,30 @@ class OrderTaxListReport(MultiSheetListExporter):
), ),
required=False, required=False,
)), )),
('date_from', forms.DateField( ('date_range',
label=_('Date from'), DateFrameField(
required=False, label=_('Date range'),
widget=DatePickerWidget, include_future_frames=False,
)), required=False,
('date_until', forms.DateField( help_text=_('Only include orders created within this date range.')
label=_('Date until'), )),
required=False,
widget=DatePickerWidget,
))
] ]
)) ))
return f return f
def filter_qs(self, qs, form_data): def filter_qs(self, qs, form_data):
date_from = form_data.get('date_from') date_range = form_data.get('date_range')
date_until = form_data.get('date_until')
date_filter = form_data.get('date_axis') date_filter = form_data.get('date_axis')
if date_from:
if isinstance(date_from, str):
date_from = parse(date_from).date()
if isinstance(date_from, date):
date_from = make_aware(datetime.combine(
date_from,
time(hour=0, minute=0, second=0, microsecond=0)
), self.event.timezone)
if date_until: if date_range:
if isinstance(date_until, str): dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.timezone)
date_until = parse(date_until).date()
if isinstance(date_until, date):
date_until = make_aware(datetime.combine(
date_until + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0)
), self.event.timezone)
if date_filter == 'order_date': if date_filter == 'order_date' and date_range:
if date_from: if dt_start:
qs = qs.filter(order__datetime__gte=date_from) qs = qs.filter(order__datetime__gte=dt_start)
if date_until: if dt_end:
qs = qs.filter(order__datetime__lt=date_until) qs = qs.filter(order__datetime__lt=dt_end)
elif date_filter == 'last_payment_date': elif date_filter == 'last_payment_date' and date_range:
p_date = OrderPayment.objects.filter( p_date = OrderPayment.objects.filter(
order=OuterRef('order'), order=OuterRef('order'),
state__in=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED], state__in=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED],
@@ -656,10 +652,10 @@ class OrderTaxListReport(MultiSheetListExporter):
m=Max('payment_date') m=Max('payment_date')
).values('m').order_by() ).values('m').order_by()
qs = qs.annotate(payment_date=Subquery(p_date, output_field=DateTimeField())) qs = qs.annotate(payment_date=Subquery(p_date, output_field=DateTimeField()))
if date_from: if dt_start:
qs = qs.filter(payment_date__gte=date_from) qs = qs.filter(payment_date__gte=dt_start)
if date_until: if dt_end:
qs = qs.filter(payment_date__lt=date_until) qs = qs.filter(payment_date__lt=dt_end)
return qs return qs
def iterate_sheet(self, form_data, sheet): def iterate_sheet(self, form_data, sheet):
+17 -30
View File
@@ -34,16 +34,14 @@
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, time, timedelta
from io import BytesIO from io import BytesIO
import dateutil.parser
from django import forms from django import forms
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import DataError, models from django.db import DataError, models
from django.db.models import OuterRef, Q, Subquery from django.db.models import OuterRef, Q, Subquery
from django.db.models.functions import Cast, Coalesce from django.db.models.functions import Cast, Coalesce
from django.utils.timezone import make_aware from django.utils.timezone import now
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from PyPDF2 import PdfMerger from PyPDF2 import PdfMerger
@@ -55,6 +53,10 @@ from pretix.base.models import (
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from ...base.services.export import ExportError from ...base.services.export import ExportError
from ...base.timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
from ...helpers.templatetags.jsonfield import JSONExtract from ...helpers.templatetags.jsonfield import JSONExtract
from .ticketoutput import PdfTicketOutput from .ticketoutput import PdfTicketOutput
@@ -78,19 +80,12 @@ class AllTicketsPDF(BaseExporter):
label=_('Include pending orders'), label=_('Include pending orders'),
required=False required=False
)), )),
('date_from', ('date_range',
forms.DateField( DateFrameField(
label=_('Start date'), label=_('Date range'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), include_future_frames=True,
required=False, required=False,
help_text=_('Only include tickets for dates on or after this date.') help_text=_('Only include tickets for dates within this range.')
)),
('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', ('order_by',
forms.ChoiceField( forms.ChoiceField(
@@ -117,8 +112,7 @@ class AllTicketsPDF(BaseExporter):
) )
if not self.is_multievent and not self.event.has_subevents: if not self.is_multievent and not self.event.has_subevents:
del d['date_from'] del d['date_range']
del d['date_to']
return d return d
@@ -135,19 +129,12 @@ class AllTicketsPDF(BaseExporter):
else: else:
qs = qs.filter(order__status__in=[Order.STATUS_PAID]) qs = qs.filter(order__status__in=[Order.STATUS_PAID])
if form_data.get('date_from'): if form_data.get('date_range'):
dt = make_aware(datetime.combine( dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
dateutil.parser.parse(form_data['date_from']).date(), if dt_start:
time(hour=0, minute=0, second=0) qs = qs.filter(Q(subevent__date_from__gte=dt_start) | Q(subevent__isnull=True, order__event__date_from__gte=dt_start))
), self.timezone) if dt_end:
qs = qs.filter(Q(subevent__date_from__gte=dt) | Q(subevent__isnull=True, order__event__date_from__gte=dt)) qs = qs.filter(Q(subevent__date_from__lt=dt_end) | Q(subevent__isnull=True, order__event__date_from__lt=dt_end))
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.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': if form_data.get('order_by') == 'name':
qs = qs.order_by('attendee_name_cached', 'order__code') qs = qs.order_by('attendee_name_cached', 'order__code')
+10 -1
View File
@@ -355,7 +355,16 @@ var form_handlers = function (el) {
var dependent = $(this), var dependent = $(this),
dependency = $($(this).attr("data-display-dependency")), dependency = $($(this).attr("data-display-dependency")),
update = function (ev) { update = function (ev) {
var enabled = dependency.toArray().some(function(d) {return (d.type === 'checkbox' || d.type === 'radio') ? d.checked : (!!d.value && !d.value.match(/^0\.?0*$/g))}); var enabled = dependency.toArray().some(function(d) {
if (d.type === 'checkbox' || d.type === 'radio') {
return d.checked;
} else if (d.type === 'select-one') {
return d.value === dependent.attr("data-display-dependency-value");
} else {
return (!!d.value && !d.value.match(/^0\.?0*$/g));
}
});
console.log(dependent, dependency, enabled)
if (dependent.is("[data-inverse]")) { if (dependent.is("[data-inverse]")) {
enabled = !enabled; enabled = !enabled;
} }
@@ -457,7 +457,7 @@ $(function () {
} }
}; };
update(); update();
dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + ']').on("change", update); dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + '], select[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update); dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
}); });
+2 -10
View File
@@ -72,19 +72,11 @@ SAMPLE_EXPORTER_CONFIG = {
"required": False "required": False
}, },
{ {
"name": "date_from", "name": "date_range",
"required": False "required": False
}, },
{ {
"name": "date_to", "name": "event_date_range",
"required": False
},
{
"name": "event_date_from",
"required": False
},
{
"name": "event_date_to",
"required": False "required": False
}, },
] ]
+110
View File
@@ -0,0 +1,110 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import date, datetime
import pytest
import pytz
from pretix.base.timeframes import (
REPORTING_DATE_TIMEFRAMES, resolve_timeframe_to_dates_inclusive,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
tz = pytz.timezone("Europe/Berlin")
def dt(*args):
return tz.localize(datetime(*args))
ref_date = date(2023, 3, 28)
@pytest.mark.parametrize("ref_dt,identifier,expected_start,expected_end,description", [
(ref_date, 'days_today', date(2023, 3, 28), date(2023, 3, 28), None),
(ref_date, 'days_yesterday', date(2023, 3, 27), date(2023, 3, 27), None),
(ref_date, 'days_last7', date(2023, 3, 22), date(2023, 3, 28), None),
(ref_date, 'days_last14', date(2023, 3, 15), date(2023, 3, 28), None),
(ref_date, 'days_tomorrow', date(2023, 3, 29), date(2023, 3, 29), None),
(ref_date, 'days_next7', date(2023, 3, 29), date(2023, 4, 4), None),
(ref_date, 'days_next14', date(2023, 3, 29), date(2023, 4, 11), None),
(ref_date, 'week_this', date(2023, 3, 27), date(2023, 4, 2), 'W 13, 2023 - March 27th April 2nd, 2023'),
(ref_date, 'week_to_date', date(2023, 3, 27), date(2023, 3, 28), 'W 13, 2023 - March 27th 28th, 2023'),
(ref_date, 'week_previous', date(2023, 3, 20), date(2023, 3, 26), 'W 12, 2023 - March 20th 26th, 2023'),
(ref_date, 'week_next', date(2023, 4, 3), date(2023, 4, 9), 'W 14, 2023 - April 3rd 9th, 2023'),
(ref_date, 'month_this', date(2023, 3, 1), date(2023, 3, 31), 'March 2023'),
(ref_date, 'month_to_date', date(2023, 3, 1), date(2023, 3, 28), 'March 2023'),
(ref_date, 'month_previous', date(2023, 2, 1), date(2023, 2, 28), 'February 2023'),
(ref_date, 'month_next', date(2023, 4, 1), date(2023, 4, 30), 'April 2023'),
(ref_date, 'quarter_this', date(2023, 1, 1), date(2023, 3, 31), 'Q1/2023'),
(ref_date, 'quarter_to_date', date(2023, 1, 1), date(2023, 3, 28), 'Q1/2023'),
(ref_date, 'quarter_previous', date(2022, 10, 1), date(2022, 12, 31), 'Q4/2022'),
(ref_date, 'quarter_next', date(2023, 4, 1), date(2023, 6, 30), 'Q2/2023'),
(ref_date, 'year_this', date(2023, 1, 1), date(2023, 12, 31), '2023'),
(ref_date, 'year_to_date', date(2023, 1, 1), date(2023, 3, 28), '2023'),
(ref_date, 'year_previous', date(2022, 1, 1), date(2022, 12, 31), '2022'),
(ref_date, 'year_next', date(2024, 1, 1), date(2024, 12, 31), '2024'),
(ref_date, 'future', date(2023, 3, 29), None, '2023-03-29 '),
(ref_date, 'past', None, date(2023, 3, 28), ' 2023-03-28'),
])
def test_timeframe(ref_dt, identifier, expected_start, expected_end, description):
for idf, label, start, end, includes_future, group, describe in REPORTING_DATE_TIMEFRAMES:
if identifier == idf:
assert start(ref_dt) == expected_start
assert end(ref_dt, expected_start) == expected_end
if expected_end and expected_start:
assert includes_future == (expected_end > ref_dt)
if description:
assert describe(expected_start, expected_end) == description
break
else:
assert False, "identifier not found"
def test_resolve():
assert resolve_timeframe_to_dates_inclusive(ref_date, "week_previous", tz) == (
date(2023, 3, 20),
date(2023, 3, 26),
)
assert resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_date, "week_previous", tz) == (
dt(2023, 3, 20, 0, 0, 0, 0),
dt(2023, 3, 27, 0, 0, 0, 0),
)
assert resolve_timeframe_to_dates_inclusive(ref_date, "2023-03-20/2023-03-21", tz) == (
date(2023, 3, 20),
date(2023, 3, 21),
)
assert resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_date, "2023-03-20/2023-03-21", tz) == (
dt(2023, 3, 20, 0, 0, 0, 0),
dt(2023, 3, 22, 0, 0, 0, 0),
)
assert resolve_timeframe_to_dates_inclusive(ref_date, "2023-03-20/", tz) == (
date(2023, 3, 20),
None
)
assert resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_date, "2023-03-20/", tz) == (
dt(2023, 3, 20, 0, 0, 0, 0),
None
)