From 4ed1b51ba2a4c1469eff66577f919129812005ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 29 Jul 2021 16:47:42 +0200 Subject: [PATCH 1/5] This time the numbers add up. --- src/backoffice/views/orga.py | 5 ++- src/tickets/models.py | 62 +++++++++++++++++++++++++----------- 2 files changed, 48 insertions(+), 19 deletions(-) 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..eb2a0450 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -6,7 +6,7 @@ import logging 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, F, OuterRef, Subquery, Sum from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ @@ -16,29 +16,55 @@ 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) - - 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) + shopticket_count = Subquery( + TicketType.objects.annotate(shopticket_count=Count("shopticket")) + .filter(pk=OuterRef("pk")) + .values("shopticket_count") ) + quantity = F("product__orderproductrelation__quantity") + + total_units_sold = Subquery( + TicketType.objects.annotate(total_units_sold=quantity) + .filter(pk=OuterRef("pk")) + .values("total_units_sold")[:1] + ) + + cost = quantity * F("product__cost") + total_cost = Subquery( + TicketType.objects.annotate(total_cost=Sum(cost)) + .filter(pk=OuterRef("pk")) + .values("total_cost")[:1] + ) + + income = quantity * F("product__price") + total_income = Subquery( + TicketType.objects.annotate(total_income=Sum(income)) + .filter(pk=OuterRef("pk")) + .values("total_income")[:1] + ) + + profit = income - cost + total_profit = Subquery( + TicketType.objects.annotate(total_profit=Sum(profit)) + .filter(pk=OuterRef("pk")) + .values("total_profit")[:1] + ) + + return self.annotate( + shopticket_count=shopticket_count, + total_units_sold=total_units_sold, + total_income=total_income, + total_cost=total_cost, + total_profit=total_profit, + ).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) From 198f6d26c709e5c6978d6d82380d1be721bb9d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 29 Jul 2021 17:08:43 +0200 Subject: [PATCH 2/5] Added avg. price per ticket per ticket type. --- src/backoffice/templates/ticket_stats.html | 2 ++ src/tickets/models.py | 10 ++++++++++ 2 files changed, 12 insertions(+) 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/tickets/models.py b/src/tickets/models.py index eb2a0450..6caf201c 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -53,12 +53,22 @@ class TicketTypeQuerySet(models.QuerySet): .values("total_profit")[:1] ) + avg_ticket_price = Subquery( + TicketType.objects.annotate(units=Sum(quantity)) + .annotate(income=Sum(income)) + .annotate(avg_ticket_price=F("income") / F("units")) + .filter(pk=OuterRef("pk")) + .values("avg_ticket_price")[:1], + output_field=models.DecimalField(), + ) + return self.annotate( shopticket_count=shopticket_count, total_units_sold=total_units_sold, total_income=total_income, total_cost=total_cost, total_profit=total_profit, + avg_ticket_price=avg_ticket_price, ).distinct() From 4ed639255401a01b261802ce4784faf5bbab9d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 29 Jul 2021 17:13:58 +0200 Subject: [PATCH 3/5] Avoid rounding down. --- src/tickets/models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tickets/models.py b/src/tickets/models.py index 6caf201c..9dffb4c7 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -2,11 +2,12 @@ import base64 import hashlib import io import logging +from decimal import Decimal import qrcode from django.conf import settings from django.db import models -from django.db.models import Count, F, OuterRef, Subquery, Sum +from django.db.models import Count, ExpressionWrapper, F, OuterRef, Subquery, Sum from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ @@ -56,7 +57,12 @@ class TicketTypeQuerySet(models.QuerySet): avg_ticket_price = Subquery( TicketType.objects.annotate(units=Sum(quantity)) .annotate(income=Sum(income)) - .annotate(avg_ticket_price=F("income") / F("units")) + .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(), From 34dcacea0cc2149c1f6bf2826adab0a3971d435b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 29 Jul 2021 20:33:43 +0200 Subject: [PATCH 4/5] DRYing the method up a bit. --- src/tickets/models.py | 57 ++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/tickets/models.py b/src/tickets/models.py index 9dffb4c7..773ea13c 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -3,11 +3,20 @@ 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, ExpressionWrapper, F, OuterRef, Subquery, 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 _ @@ -19,40 +28,16 @@ logger = logging.getLogger("bornhack.%s" % __name__) class TicketTypeQuerySet(models.QuerySet): def with_price_stats(self): - shopticket_count = Subquery( - TicketType.objects.annotate(shopticket_count=Count("shopticket")) - .filter(pk=OuterRef("pk")) - .values("shopticket_count") - ) + def _make_subquery(annotation: Union[Expression, F]) -> Subquery: + return Subquery( + TicketType.objects.annotate(annotation_value=annotation) + .filter(pk=OuterRef("pk")) + .values("annotation_value")[:1] + ) quantity = F("product__orderproductrelation__quantity") - - total_units_sold = Subquery( - TicketType.objects.annotate(total_units_sold=quantity) - .filter(pk=OuterRef("pk")) - .values("total_units_sold")[:1] - ) - cost = quantity * F("product__cost") - total_cost = Subquery( - TicketType.objects.annotate(total_cost=Sum(cost)) - .filter(pk=OuterRef("pk")) - .values("total_cost")[:1] - ) - income = quantity * F("product__price") - total_income = Subquery( - TicketType.objects.annotate(total_income=Sum(income)) - .filter(pk=OuterRef("pk")) - .values("total_income")[:1] - ) - - profit = income - cost - total_profit = Subquery( - TicketType.objects.annotate(total_profit=Sum(profit)) - .filter(pk=OuterRef("pk")) - .values("total_profit")[:1] - ) avg_ticket_price = Subquery( TicketType.objects.annotate(units=Sum(quantity)) @@ -69,11 +54,11 @@ class TicketTypeQuerySet(models.QuerySet): ) return self.annotate( - shopticket_count=shopticket_count, - total_units_sold=total_units_sold, - total_income=total_income, - total_cost=total_cost, - total_profit=total_profit, + shopticket_count=_make_subquery(Count("shopticket")), + total_units_sold=_make_subquery(quantity), + total_income=_make_subquery(cost), + total_cost=_make_subquery(cost), + total_profit=_make_subquery(income - cost), avg_ticket_price=avg_ticket_price, ).distinct() From 1889d3bf15012174a5a52751b80939850346b6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 29 Jul 2021 20:35:14 +0200 Subject: [PATCH 5/5] income not cost. --- src/tickets/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tickets/models.py b/src/tickets/models.py index 773ea13c..5e2035be 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -56,7 +56,7 @@ class TicketTypeQuerySet(models.QuerySet): return self.annotate( shopticket_count=_make_subquery(Count("shopticket")), total_units_sold=_make_subquery(quantity), - total_income=_make_subquery(cost), + total_income=_make_subquery(income), total_cost=_make_subquery(cost), total_profit=_make_subquery(income - cost), avg_ticket_price=avg_ticket_price,