add detailed ticket stats view
This commit is contained in:
parent
a8c4894b8f
commit
5c410939ed
|
@ -17,15 +17,17 @@
|
||||||
<th class="text-center">Number Sold</th>
|
<th class="text-center">Number Sold</th>
|
||||||
<th class="text-right">Total Income</th>
|
<th class="text-right">Total Income</th>
|
||||||
<th class="text-right">Average Price</th>
|
<th class="text-right">Average Price</th>
|
||||||
|
<th class="text-center">Products</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for tt in tickettype_list %}
|
{% for tt in tickettype_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ tt.name }}</td>
|
<td><a href="{% url 'backoffice:shop_ticket_stats_detail' camp_slug=camp.slug pk=tt.pk %}">{{ tt.name }}</a></td>
|
||||||
<td class="text-center">{{ tt.shopticket_count }}</td>
|
<td class="text-center">{{ tt.shopticket_count }}</td>
|
||||||
<td class="text-right">{{ tt.total_price|floatformat:"2" }} DKK</td>
|
<td class="text-right">{{ tt.total_price|floatformat:"2" }} DKK</td>
|
||||||
<td class="text-right">{{ tt.average_price|floatformat:"2" }} DKK</td>
|
<td class="text-right">{{ tt.average_price|floatformat:"2" }} DKK</td>
|
||||||
|
<td class="text-center">{{ tt.product_set.count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
41
src/backoffice/templates/ticket_stats_detail.html
Normal file
41
src/backoffice/templates/ticket_stats_detail.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load static %}
|
||||||
|
{% load bornhack %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="h3">BackOffice - {{ camp.title }} {{ product_list.0.ticket_type.name }} Ticket Product Stats</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="lead">This view shows a table of each product associated with the ticket type <b>{{ product_list.0.ticket_type.name }}</b> along with the number sold, cost, price, profit and margin for each product.</p>
|
||||||
|
<table class="table table-hover datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th class="text-center">Number Sold</th>
|
||||||
|
<th class="text-right">Revenue</th>
|
||||||
|
<th class="text-right">Cost</th>
|
||||||
|
<th class="text-right">Profit</th>
|
||||||
|
<th class="text-right">Margin</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in product_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.name }}</td>
|
||||||
|
<td class="text-center">{{ p.ticket_count }}</td>
|
||||||
|
<td class="text-right">{{ p.price|floatformat:"2" }} DKK</td>
|
||||||
|
<td class="text-right">{{ p.cost|floatformat:"2" }} DKK</td>
|
||||||
|
<td class="text-right">{{ p.profit|floatformat:"2" }} DKK</td>
|
||||||
|
<td class="text-right">{{ p.margin }}%</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<a class="btn btn-default" href="{% url 'backoffice:shop_ticket_stats' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back to Ticket Typess</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -85,6 +85,7 @@ from .views import (
|
||||||
RevenueListView,
|
RevenueListView,
|
||||||
ScanTicketsView,
|
ScanTicketsView,
|
||||||
ShopTicketOverview,
|
ShopTicketOverview,
|
||||||
|
ShopTicketStatsDetailView,
|
||||||
ShopTicketStatsView,
|
ShopTicketStatsView,
|
||||||
SpeakerDeleteView,
|
SpeakerDeleteView,
|
||||||
SpeakerDetailView,
|
SpeakerDetailView,
|
||||||
|
@ -811,6 +812,15 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"shop_ticket_stats/",
|
"shop_ticket_stats/",
|
||||||
include([path("", ShopTicketStatsView.as_view(), name="shop_ticket_stats")]),
|
include(
|
||||||
|
[
|
||||||
|
path("", ShopTicketStatsView.as_view(), name="shop_ticket_stats"),
|
||||||
|
path(
|
||||||
|
"<uuid:pk>/",
|
||||||
|
ShopTicketStatsDetailView.as_view(),
|
||||||
|
name="shop_ticket_stats_detail",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from camps.mixins import CampViewMixin
|
from camps.mixins import CampViewMixin
|
||||||
from profiles.models import Profile
|
from profiles.models import Profile
|
||||||
from shop.models import OrderProductRelation
|
from shop.models import OrderProductRelation, Product
|
||||||
from teams.models import Team
|
from teams.models import Team
|
||||||
from tickets.models import TicketType
|
from tickets.models import TicketType
|
||||||
from utils.models import OutgoingEmail
|
from utils.models import OutgoingEmail
|
||||||
|
@ -204,3 +204,13 @@ class ShopTicketStatsView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return TicketType.objects.with_price_stats().filter(camp=self.camp)
|
return TicketType.objects.with_price_stats().filter(camp=self.camp)
|
||||||
|
|
||||||
|
|
||||||
|
class ShopTicketStatsDetailView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
|
||||||
|
model = Product
|
||||||
|
template_name = "ticket_stats_detail.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Product.statsobjects.with_ticket_stats().filter(
|
||||||
|
ticket_type_id=self.kwargs["pk"]
|
||||||
|
)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.contrib import messages
|
||||||
from django.contrib.postgres.fields import DateTimeRangeField
|
from django.contrib.postgres.fields import DateTimeRangeField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.aggregates import Sum
|
from django.db.models.aggregates import Count, Sum
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
|
@ -394,6 +394,11 @@ class ProductCategory(CreatedUpdatedModel, UUIDModel):
|
||||||
super().save(**kwargs)
|
super().save(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductStatsManager(models.Manager):
|
||||||
|
def with_ticket_stats(self):
|
||||||
|
return self.annotate(ticket_count=Count("shopticket")).exclude(ticket_count=0)
|
||||||
|
|
||||||
|
|
||||||
class Product(CreatedUpdatedModel, UUIDModel):
|
class Product(CreatedUpdatedModel, UUIDModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Product"
|
verbose_name = "Product"
|
||||||
|
@ -443,6 +448,7 @@ class Product(CreatedUpdatedModel, UUIDModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ProductQuerySet.as_manager()
|
objects = ProductQuerySet.as_manager()
|
||||||
|
statsobjects = ProductStatsManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} ({} DKK)".format(self.name, self.price)
|
return "{} ({} DKK)".format(self.name, self.price)
|
||||||
|
@ -513,6 +519,13 @@ class Product(CreatedUpdatedModel, UUIDModel):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def margin(self):
|
||||||
|
try:
|
||||||
|
return (self.price / self.profit) * 100
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class OrderProductRelation(CreatedUpdatedModel):
|
class OrderProductRelation(CreatedUpdatedModel):
|
||||||
order = models.ForeignKey("shop.Order", on_delete=models.PROTECT)
|
order = models.ForeignKey("shop.Order", on_delete=models.PROTECT)
|
||||||
|
|
Loading…
Reference in a new issue