forked from CGM_Public/pretix_original
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user