diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index df968fbc7..ced8fd7fa 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -385,7 +385,7 @@ class Event(EventMixin, LoggedModel): if img: return urljoin(build_absolute_uri(self, 'presale:event.index'), img) - def free_seats(self, ignore_voucher=None, sales_channel='web'): + def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False): from .orders import CartPosition, Order, OrderPosition from .vouchers import Voucher vqs = Voucher.objects.filter( @@ -416,7 +416,7 @@ class Event(EventMixin, LoggedModel): vqs ) ).filter(has_order=False, has_cart=False, has_voucher=False) - if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel: + if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked): qs = qs.filter(blocked=False) return qs @@ -1032,7 +1032,7 @@ class SubEvent(EventMixin, LoggedModel): def __str__(self): return '{} - {}'.format(self.name, self.get_date_range_display()) - def free_seats(self, ignore_voucher=None, sales_channel='web'): + def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False): from .orders import CartPosition, Order, OrderPosition from .vouchers import Voucher vqs = Voucher.objects.filter( @@ -1066,7 +1066,7 @@ class SubEvent(EventMixin, LoggedModel): vqs ) ).filter(has_order=False, has_cart=False, has_voucher=False) - if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel: + if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked): qs = qs.filter(blocked=False) return qs diff --git a/src/pretix/plugins/statistics/templates/pretixplugins/statistics/index.html b/src/pretix/plugins/statistics/templates/pretixplugins/statistics/index.html index 71b16389e..0af5cbe97 100644 --- a/src/pretix/plugins/statistics/templates/pretixplugins/statistics/index.html +++ b/src/pretix/plugins/statistics/templates/pretixplugins/statistics/index.html @@ -3,6 +3,8 @@ {% load compress %} {% load static %} {% load escapejson %} +{% load money %} +{% load getitem %} {% block title %}{% trans "Statistics" %}{% endblock %} {% block content %}

{% trans "Statistics" %}

@@ -59,6 +61,102 @@
+ {% if seats %} +
+
+

{% trans "Seating Overview" %}

+
+
+
+
+
+ {{ seats.purchased_seats }} + {% trans "Sold Seats" %} +
+
+
+
+ {{ seats.blocked_seats }} + {% trans "Blocked Seats" %} +
+
+
+
+ {{ seats.free_seats }} + {% trans "Free Seats" %} +
+
+
+
+
+
+
+

{% trans "Seating Sales Potentials" %}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + {% for item, props in seats.products.items %} + {% if item is not None %} + + + + + + + + + {% endif %} + {% endfor %} + + + + + + + + + {% if None in seats.products %} + {% with seats.products|getitem:None as unattributed %} + + + + + + + + + {% endwith %} + {% endif %} + +
{% trans "Product" %}{% trans "Unsold Seats" %}{% trans "Potential Profits" %}
{% trans "Minimum Price" %}{% trans "Blocked" %}{% trans "Available" %}{% trans "Blocked" %}{% trans "Available" %}
{% trans "On Sale" %}
{{ item }}{{ props.price|money:request.event.currency }}{{ props.blocked.seats }}{{ props.free.seats }}{{ props.blocked.potential|money:request.event.currency }}{{ props.free.potential|money:request.event.currency }}
{% trans "Not on Sale" %}
{% trans "Seats not attributed to any specific product" %}{{ unattributed.blocked.seats }}{{ unattributed.free.seats }}
+
+
+ {% endif %} diff --git a/src/pretix/plugins/statistics/views.py b/src/pretix/plugins/statistics/views.py index 94eb756d1..578439ef2 100644 --- a/src/pretix/plugins/statistics/views.py +++ b/src/pretix/plugins/statistics/views.py @@ -1,9 +1,10 @@ import datetime import json +from decimal import Decimal import dateutil.parser import dateutil.rrule -from django.db.models import Count, DateTimeField, Max, OuterRef, Subquery +from django.db.models import Count, DateTimeField, Max, Min, OuterRef, Subquery from django.utils import timezone from django.views.generic import TemplateView @@ -162,4 +163,56 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView) ctx['has_orders'] = self.request.event.orders.exists() + ctx['seats'] = {} + + if not self.request.event.has_subevents or (ckey != "all" and subevent): + ev = subevent or self.request.event + if ev.seating_plan_id is not None: + seats_qs = ev.free_seats(sales_channel=None, include_blocked=True) + ctx['seats']['blocked_seats'] = seats_qs.filter(blocked=True).count() + ctx['seats']['free_seats'] = seats_qs.filter(blocked=False).count() + ctx['seats']['purchased_seats'] = \ + ev.seats.count() - ctx['seats']['blocked_seats'] - ctx['seats']['free_seats'] + + seats_qs = seats_qs.values('product', 'blocked').annotate(count=Count('id'))\ + .order_by('product__category__position', 'product__position', 'product', 'blocked') + + ctx['seats']['products'] = {} + ctx['seats']['stats'] = {} + item_cache = {i.pk: i for i in + ev.items.annotate(has_variations=Count('variations')).filter( + pk__in={p['product'] for p in seats_qs if p['product']} + )} + item_cache[None] = None + + for item in seats_qs: + if item_cache[item['product']] not in ctx['seats']['products']: + product = item_cache[item['product']] + if product and product.has_variations: + price = product.variations.aggregate(Min('default_price'))['default_price__min'] + elif product: + price = product.default_price + else: + price = Decimal('0.00') + + ctx['seats']['products'][product] = { + 'free': { + 'seats': 0, + 'potential': Decimal('0.00'), + }, + 'blocked': { + 'seats': 0, + 'potential': Decimal('0.00'), + }, + 'price': price, + } + data = ctx['seats']['products'][product] + + if item['blocked']: + data['blocked']['seats'] = item['count'] + data['blocked']['potential'] = item['count'] * data['price'] + else: + data['free']['seats'] = item['count'] + data['free']['potential'] = item['count'] * data['price'] + return ctx