Files
pretix_original/src/pretix/base/services/stats.py
2024-01-12 15:55:27 +01:00

319 lines
13 KiB
Python

#
# 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/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Flavia Bastos, Tobias Kunze
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
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, DateTimeField, F, Max, OuterRef, QuerySet, Subquery, Sum,
Value, When,
)
from django.utils.timezone import make_aware
from django.utils.translation import gettext_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, OrderPayment
from pretix.base.signals import order_fee_type_name
class DummyObject:
def __str__(self):
return str(self.name)
class Dontsum:
def __init__(self, value: Any):
self.value = value
def __str__(self) -> str:
return str(self.value)
def tuplesum(tuples: Iterable[Tuple]) -> Tuple:
"""
Takes a list of tuples of size n. In our case, those are e.g. tuples of size 2 containing
a number of sales and a sum of their toal amount.
Returned is again a tuple of size n. The first component of the returned tuple is the
sum of the first components of all input tuples.
Sample:
>>> tuplesum([(1, 2), (3, 4), (5, 6)])
(9, 12)
"""
def mysum(it):
# This method is identical to sum(list), except that it ignores entries of the type
# Dontsum. We need this because we list the payment method fees seperately but we don't
# want a order to contribute twice to the total count of orders (once for a product
# and once for the payment method fee).
sit = [i for i in it if not isinstance(i, Dontsum)]
return sum(sit)
# zip(*list(tuples)) basically transposes our input, e.g. [(1,2), (3,4), (5,6)]
# becomes [(1, 3, 5), (2, 4, 6)]. We then call map on that, such that mysum((1, 3, 5))
# and mysum((2, 4, 6)) will be called. The results will then be combined in a tuple again.
return tuple(map(mysum, zip(*list(tuples))))
def dictsum(*dicts) -> dict:
"""
Takes multiple dictionaries as arguments and builds a new dict. The input dict is expected
to be a mapping of keys to tuples. The output dict will contain all keys that are
present in any of the input dicts and will contain the tuplesum of all values associated
with this key (see tuplesum function).
Sample:
>>> dictsum({'a': (1, 2), 'b': (3, 4)}, {'a': (5, 6), 'c': (7, 8)})
{'a': (6, 8), 'b': (3, 4), 'c': (7, 8)}
"""
res = {}
keys = set()
for d in dicts:
keys |= set(d.keys())
for k in keys:
res[k] = tuplesum(d[k] for d in dicts if k in d)
return res
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_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(
'variations'
).order_by('category__position', 'category_id', 'position', 'name')
qs = OrderPosition.all if base_qs is None else base_qs
if isinstance(subevent, (list, QuerySet)):
qs = qs.filter(subevent__in=subevent)
elif subevent:
qs = qs.filter(subevent=subevent)
if subevent_date_from:
qs = qs.filter(subevent__date_from__gte=subevent_date_from)
if subevent_date_until:
qs = qs.filter(subevent__date_from__lt=subevent_date_until)
if admission_only:
qs = qs.filter(item__admission=True)
items = items.filter(admission=True)
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(
status=Case(
When(order__status='n', order__require_approval=True, then=Value('unapproved')),
When(canceled=True, then=Value('c')),
default=F('order__status')
)
).values(
'item', 'variation', 'status'
).annotate(cnt=Count('id'), price=Sum('price'), tax_value=Sum('tax_value')).order_by()
states = {
'unapproved': 'unapproved',
'canceled': Order.STATUS_CANCELED,
'paid': Order.STATUS_PAID,
'pending': Order.STATUS_PENDING,
'expired': Order.STATUS_EXPIRED,
}
num = {}
for l, s in states.items():
num[l] = {
(p['item'], p['variation']): (p['cnt'], p['price'], p['price'] - p['tax_value'])
for p in counters if p['status'] == s
}
num['total'] = dictsum(num['pending'], num['paid'])
for item in items:
item.all_variations = list(item.variations.all())
item.has_variations = (len(item.all_variations) > 0)
item.num = {}
if item.has_variations:
for var in item.all_variations:
variid = var.id
var.num = {}
for l in states.keys():
var.num[l] = num[l].get((item.id, variid), (0, 0, 0))
var.num['total'] = num['total'].get((item.id, variid), (0, 0, 0))
for l in states.keys():
item.num[l] = tuplesum(var.num[l] for var in item.all_variations)
item.num['total'] = tuplesum(var.num['total'] for var in item.all_variations)
else:
for l in states.keys():
item.num[l] = num[l].get((item.id, None), (0, 0, 0))
item.num['total'] = num['total'].get((item.id, None), (0, 0, 0))
nonecat = ItemCategory(name=_('Uncategorized'))
# Regroup those by category
items_by_category = sorted(
[
# a group is a tuple of a category and a list of items
(cat if cat is not None else nonecat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items])
# insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
],
key=lambda group: (group[0].position, group[0].id) if (
group[0] is not None and group[0].id is not None) else (0, 0)
)
for c in items_by_category:
c[0].num = {}
for l in states.keys():
c[0].num[l] = tuplesum(item.num[l] for item in c[1])
c[0].num['total'] = tuplesum(item.num['total'] for item in c[1])
# Payment fees
payment_cat_obj = DummyObject()
payment_cat_obj.name = _('Fees')
payment_items = []
if subevent is None and not subevent_date_from and not subevent_date_until and fees:
qs = OrderFee.all if base_fees_qs is None else base_fees_qs
qs = qs.filter(
order__event=event
).annotate(
status=Case(
When(order__status='n', order__require_approval=True, then=Value('unapproved')),
When(canceled=True, then=Value('c')),
default=F('order__status')
)
)
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()
for l, s in states.items():
num[l] = {
(o['fee_type'], o['internal_type']): (o['cnt'], o['value'], o['value'] - o['tax_value'])
for o in counters if o['status'] == s
}
num['total'] = dictsum(num['pending'], num['paid'])
provider_names = {
k: v.verbose_name
for k, v in event.get_payment_providers().items()
}
names = dict(OrderFee.FEE_TYPES)
for pprov, total in sorted(num['total'].items(), key=lambda i: i[0]):
ppobj = DummyObject()
if pprov[0] == OrderFee.FEE_TYPE_PAYMENT:
ppobj.name = '{} - {}'.format(names[pprov[0]], provider_names.get(pprov[1], pprov[1]))
else:
name = pprov[1]
for r, resp in order_fee_type_name.send(sender=event, fee_type=pprov[0], internal_type=pprov[1]):
if resp:
name = resp
break
ppobj.name = '{} - {}'.format(names[pprov[0]], name)
ppobj.provider = pprov[1]
ppobj.has_variations = False
ppobj.num = {}
for l in states.keys():
ppobj.num[l] = num[l].get(pprov, (0, 0, 0))
ppobj.num['total'] = total
payment_items.append(ppobj)
payment_cat_obj.num = {}
for l in states.keys():
payment_cat_obj.num[l] = (
Dontsum(''), sum(i.num[l][1] for i in payment_items), sum(i.num[l][2] for i in payment_items)
)
payment_cat_obj.num['total'] = (
Dontsum(''), sum(i.num['total'][1] for i in payment_items), sum(i.num['total'][2] for i in payment_items)
)
payment_cat = (payment_cat_obj, payment_items)
any_payment = any(payment_cat_obj.num[s][1] for s in states.keys())
if any_payment:
items_by_category.append(payment_cat)
total = {
'num': {'total': tuplesum(c.num['total'] for c, i in items_by_category)}
}
for l in states.keys():
total['num'][l] = tuplesum(c.num[l] for c, i in items_by_category)
return items_by_category, total