mirror of
https://github.com/pretix/pretix.git
synced 2026-01-05 21:32:26 +00:00
276 lines
10 KiB
Python
276 lines
10 KiB
Python
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, 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
|
|
) -> 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 subevent:
|
|
qs = qs.filter(subevent=subevent)
|
|
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 not subevent and fees:
|
|
qs = OrderFee.all.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
|