diff --git a/src/backoffice/templates/ticket_stats.html b/src/backoffice/templates/ticket_stats.html
index ba890026..a4c2ad20 100644
--- a/src/backoffice/templates/ticket_stats.html
+++ b/src/backoffice/templates/ticket_stats.html
@@ -20,6 +20,7 @@
Total Income |
Total Cost |
Total Profit |
+ Avg. Ticket Price |
@@ -32,6 +33,7 @@
{{ tt.total_income|floatformat:"2" }} DKK |
{{ tt.total_cost|floatformat:"2" }} DKK |
{{ tt.total_profit|floatformat:"2" }} DKK |
+ {{ tt.avg_ticket_price|floatformat:"2" }} DKK |
{% endfor %}
diff --git a/src/backoffice/views/orga.py b/src/backoffice/views/orga.py
index d246c7a5..bc5cbc81 100644
--- a/src/backoffice/views/orga.py
+++ b/src/backoffice/views/orga.py
@@ -203,7 +203,10 @@ class ShopTicketStatsView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
template_name = "ticket_stats.html"
def get_queryset(self):
- return TicketType.objects.with_price_stats().filter(camp=self.camp)
+ query = TicketType.objects.filter(
+ camp=self.camp, shopticket__isnull=False
+ ).with_price_stats()
+ return query
class ShopTicketStatsDetailView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
diff --git a/src/tickets/models.py b/src/tickets/models.py
index 10afe064..5e2035be 100644
--- a/src/tickets/models.py
+++ b/src/tickets/models.py
@@ -2,11 +2,21 @@ import base64
import hashlib
import io
import logging
+from decimal import Decimal
+from typing import Union
import qrcode
from django.conf import settings
from django.db import models
-from django.db.models import Count, F, Sum
+from django.db.models import (
+ Count,
+ Expression,
+ ExpressionWrapper,
+ F,
+ OuterRef,
+ Subquery,
+ Sum,
+)
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
@@ -16,29 +26,46 @@ from utils.pdf import generate_pdf_letter
logger = logging.getLogger("bornhack.%s" % __name__)
-class TicketTypeManager(models.Manager):
+class TicketTypeQuerySet(models.QuerySet):
def with_price_stats(self):
- total_units_sold = Sum("shopticket__opr__quantity", distinct=True)
- cost = F("shopticket__opr__quantity") * F("shopticket__opr__product__cost")
- income = F("shopticket__opr__quantity") * F("shopticket__opr__product__price")
- profit = income - cost
- total_cost = Sum(cost, distinct=True)
- total_profit = Sum(profit, distinct=True)
- total_income = Sum(income, distinct=True)
+ def _make_subquery(annotation: Union[Expression, F]) -> Subquery:
+ return Subquery(
+ TicketType.objects.annotate(annotation_value=annotation)
+ .filter(pk=OuterRef("pk"))
+ .values("annotation_value")[:1]
+ )
- return (
- self.filter(shopticket__isnull=False)
- .annotate(shopticket_count=Count("shopticket"))
- .annotate(total_units_sold=total_units_sold)
- .annotate(total_income=total_income)
- .annotate(total_cost=total_cost)
- .annotate(total_profit=total_profit)
+ quantity = F("product__orderproductrelation__quantity")
+ cost = quantity * F("product__cost")
+ income = quantity * F("product__price")
+
+ avg_ticket_price = Subquery(
+ TicketType.objects.annotate(units=Sum(quantity))
+ .annotate(income=Sum(income))
+ .annotate(
+ avg_ticket_price=ExpressionWrapper(
+ F("income") * Decimal("1.0") / F("units"),
+ output_field=models.DecimalField(),
+ )
+ )
+ .filter(pk=OuterRef("pk"))
+ .values("avg_ticket_price")[:1],
+ output_field=models.DecimalField(),
)
+ return self.annotate(
+ shopticket_count=_make_subquery(Count("shopticket")),
+ total_units_sold=_make_subquery(quantity),
+ total_income=_make_subquery(income),
+ total_cost=_make_subquery(cost),
+ total_profit=_make_subquery(income - cost),
+ avg_ticket_price=avg_ticket_price,
+ ).distinct()
+
# TicketType can be full week, one day, cabins, parking, merch, hax, massage, etc.
class TicketType(CampRelatedModel, UUIDModel):
- objects = TicketTypeManager()
+ objects = TicketTypeQuerySet.as_manager()
name = models.TextField()
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
includes_badge = models.BooleanField(default=False)