Statistics on sold and unsold seats, as well as potential profi… (#1613)

* Statistics on sold and unsold seats, as well as potential profits

* Rework of seats-stats

* Fix crash when all seats are assigned

* Update src/pretix/plugins/statistics/views.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Update src/pretix/plugins/statistics/views.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Update src/pretix/plugins/statistics/views.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Fix count of sold seats

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Martin Gross
2020-03-16 15:20:30 +01:00
committed by GitHub
parent bd238f76ce
commit f00012a63e
3 changed files with 156 additions and 5 deletions

View File

@@ -385,7 +385,7 @@ class Event(EventMixin, LoggedModel):
if img: if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), 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 .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher from .vouchers import Voucher
vqs = Voucher.objects.filter( vqs = Voucher.objects.filter(
@@ -416,7 +416,7 @@ class Event(EventMixin, LoggedModel):
vqs vqs
) )
).filter(has_order=False, has_cart=False, has_voucher=False) ).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) qs = qs.filter(blocked=False)
return qs return qs
@@ -1032,7 +1032,7 @@ class SubEvent(EventMixin, LoggedModel):
def __str__(self): def __str__(self):
return '{} - {}'.format(self.name, self.get_date_range_display()) 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 .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher from .vouchers import Voucher
vqs = Voucher.objects.filter( vqs = Voucher.objects.filter(
@@ -1066,7 +1066,7 @@ class SubEvent(EventMixin, LoggedModel):
vqs vqs
) )
).filter(has_order=False, has_cart=False, has_voucher=False) ).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) qs = qs.filter(blocked=False)
return qs return qs

View File

@@ -3,6 +3,8 @@
{% load compress %} {% load compress %}
{% load static %} {% load static %}
{% load escapejson %} {% load escapejson %}
{% load money %}
{% load getitem %}
{% block title %}{% trans "Statistics" %}{% endblock %} {% block title %}{% trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Statistics" %}</h1> <h1>{% trans "Statistics" %}</h1>
@@ -59,6 +61,102 @@
<div id="obp_chart" class="chart"></div> <div id="obp_chart" class="chart"></div>
</div> </div>
</div> </div>
{% if seats %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Seating Overview" %}</h3>
</div>
<div class="panel-body">
<div class="dashboard">
<div class="widget-container widget-small">
<div class="numwidget">
<span class="num">{{ seats.purchased_seats }}</span>
<span class="text">{% trans "Sold Seats" %}</span>
</div>
</div>
<div class="widget-container widget-small">
<div class="numwidget">
<span class="num">{{ seats.blocked_seats }}</span>
<span class="text">{% trans "Blocked Seats" %}</span>
</div>
</div>
<div class="widget-container widget-small">
<div class="numwidget">
<span class="num">{{ seats.free_seats }}</span>
<span class="text">{% trans "Free Seats" %}</span>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Seating Sales Potentials" %}</h3>
</div>
<div class="panel-body">
<table class="table table-condensed table-hover table-product-overview">
<thead>
<tr>
<th>{% trans "Product" %}</th>
<th></th>
<th colspan="2" class="text-center">{% trans "Unsold Seats" %}</th>
<th colspan="2" class="text-center">{% trans "Potential Profits" %}</th>
</tr>
<tr>
<th></th>
<th>{% trans "Minimum Price" %}</th>
<th>{% trans "Blocked" %}</th>
<th>{% trans "Available" %}</th>
<th>{% trans "Blocked" %}</th>
<th>{% trans "Available" %}</th>
</tr>
</thead>
<tbody>
<tr class="category">
<th>{% trans "On Sale" %}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
{% for item, props in seats.products.items %}
{% if item is not None %}
<tr class="item categorized">
<td>{{ item }}</td>
<td>{{ props.price|money:request.event.currency }}</td>
<td>{{ props.blocked.seats }}</td>
<td>{{ props.free.seats }}</td>
<td>{{ props.blocked.potential|money:request.event.currency }}</td>
<td>{{ props.free.potential|money:request.event.currency }}</td>
</tr>
{% endif %}
{% endfor %}
<tr class="category">
<th>{% trans "Not on Sale" %}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
{% if None in seats.products %}
{% with seats.products|getitem:None as unattributed %}
<tr class="item categorized">
<td>{% trans "Seats not attributed to any specific product" %}</td>
<td></td>
<td>{{ unattributed.blocked.seats }}</td>
<td>{{ unattributed.free.seats }}</td>
<td></td>
<td></td>
</tr>
{% endwith %}
{% endif %}
</tbody>
</table>
</div>
</div>
{% endif %}
<script type="application/json" id="obd-data">{{ obd_data|escapejson }}</script> <script type="application/json" id="obd-data">{{ obd_data|escapejson }}</script>
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script> <script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
<script type="application/json" id="obp-data">{{ obp_data|escapejson }}</script> <script type="application/json" id="obp-data">{{ obp_data|escapejson }}</script>

View File

@@ -1,9 +1,10 @@
import datetime import datetime
import json import json
from decimal import Decimal
import dateutil.parser import dateutil.parser
import dateutil.rrule 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.utils import timezone
from django.views.generic import TemplateView from django.views.generic import TemplateView
@@ -162,4 +163,56 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
ctx['has_orders'] = self.request.event.orders.exists() 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 return ctx