remove-useless-queries-counter-stats (#519)

This commit is contained in:
thomas girod 2023-03-24 15:32:05 +01:00 committed by GitHub
parent f0a08afd31
commit 6c1fa6de0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 790 additions and 558 deletions

View File

@ -1197,19 +1197,39 @@ blockquote h5:first-child {
color: red; color: red;
} }
table {
width: 90%;
margin: 15px auto;
border-collapse: collapse;
border-spacing: 0;
border-radius: 5px;
-moz-border-radius: 5px;
overflow: hidden;
box-shadow: rgba(60, 64, 67, .3) 0 1px 3px 0, rgba(60, 64, 67, .15) 0 4px 8px 3px;
}
@media screen and (max-width: 500px){
table { table {
width: 100%; width: 100%;
font-size: 0.9em; }
} }
th { th {
padding: 4px; padding: 4px;
} }
td, th {
vertical-align: middle;
text-align: center;
padding: 5px 10px;
> ul {
margin-top: 0;
}
}
td { td {
padding: 4px; padding: 4px;
margin: 5px; margin: 5px;
border: solid 1px $primary-neutral-color;
border-collapse: collapse; border-collapse: collapse;
vertical-align: top; vertical-align: top;
overflow: hidden; overflow: hidden;
@ -1219,18 +1239,29 @@ td {
} }
} }
th, thead td {
text-align: center;
border-top: none;
}
thead { thead {
font-weight: bold; background-color: #354a5f;
color: white;
} }
tbody > tr { tbody > tr {
&:nth-child(even) { &:nth-child(even) {
background: $primary-neutral-light-color; background: $primary-neutral-light-color;
} }
&:hover { &.clickable:hover {
cursor: pointer;
background: $secondary-neutral-light-color; background: $secondary-neutral-light-color;
width: 100%; width: 100%;
} }
&.highlight {
color: $primary-dark-color;
font-style: italic;
}
} }
sup { sup {

View File

@ -5,8 +5,12 @@
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s profile{% endtrans %} {% trans user_name=profile.get_display_name() %}{{ user_name }}'s profile{% endtrans %}
{% endblock %} {% endblock %}
{% block additional_js %}
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
{% endblock %}
{% block content %} {% block content %}
<div id="user_profile_page"> <div id="user_profile_page" x-data>
<div id="user_profile"> <div id="user_profile">
<!-- Profile --> <!-- Profile -->
<div id="user_profile_infos"> <div id="user_profile_infos">
@ -146,15 +150,25 @@
</div> </div>
{% endif %} {% endif %}
{% if profile.was_subscribed and (user == profile or user.can_read_subscription_history)%} {% if profile.was_subscribed and (user == profile or user.can_read_subscription_history)%}
<div id="drop_subscriptions"> <div class="collapse" :class="{'shadow': collapsed}" x-data="{collapsed: false}" x-cloak>
<h5>{% trans %}Subscription history{% endtrans %}</h5> <div class="collapse-header clickable" @click="collapsed = !collapsed">
<span class="collapse-header-text">
{% trans %}Subscription history{% endtrans %}
</span>
<span class="collapse-header-icon" :class="{'reverse': collapsed}">
<i class="fa fa-caret-down"></i>
</span>
</div>
<div class="collapse-body" x-show="collapsed" x-transition.scale.origin.top>
<table> <table>
<thead>
<tr> <tr>
<th>{% trans %}Subscription start{% endtrans %}</th> <th>{% trans %}Subscription start{% endtrans %}</th>
<th>{% trans %}Subscription end{% endtrans %}</th> <th>{% trans %}Subscription end{% endtrans %}</th>
<th>{% trans %}Subscription type{% endtrans %}</th> <th>{% trans %}Subscription type{% endtrans %}</th>
<th>{% trans %}Payment method{% endtrans %}</th> <th>{% trans %}Payment method{% endtrans %}</th>
</tr> </tr>
</thead>
{% for sub in profile.subscriptions.all() %} {% for sub in profile.subscriptions.all() %}
<tr> <tr>
<td>{{ sub.subscription_start }}</td> <td>{{ sub.subscription_start }}</td>
@ -165,6 +179,7 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
</div>
{% endif %} {% endif %}
{% if user.is_root or user.is_board_member %} {% if user.is_root or user.is_board_member %}
@ -177,13 +192,25 @@
<input type="submit" value="{% trans %}Give gift{% endtrans %}"> <input type="submit" value="{% trans %}Give gift{% endtrans %}">
</form> </form>
{% if profile.gifts.exists() %} {% if profile.gifts.exists() %}
{% set gifts = profile.gifts.order_by("-date")|list %}
<br> <br>
<div id="drop_gifts"> <div class="collapse" :class="{'shadow': collapsed}" x-data="{collapsed: false}" x-cloak>
<h5>{% trans %}Last given gift :{% endtrans %} {{ profile.gifts.order_by('-date').first() }}</h5> <div class="collapse-header clickable" @click="collapsed = !collapsed">
<div> <span class="collapse-header-text">
{% trans %}Last given gift :{% endtrans %} {{ gifts[0] }}
</span>
<span class="collapse-header-icon" :class="{'reverse': collapsed}">
<i class="fa fa-caret-down"></i>
</span>
</div>
<div class="collapse-body" x-show="collapsed" x-transition.scale.origin.top>
<ul> <ul>
{% for gift in profile.gifts.all().order_by('-date') %} {% for gift in gifts %}
<li>{{ gift }} <a href="{{ url('core:user_gift_delete', user_id=profile.id, gift_id=gift.id) }}">{% trans %}Delete{% endtrans %}</a></li> <li>{{ gift }}
<a href="{{ url('core:user_gift_delete', user_id=profile.id, gift_id=gift.id) }}">
<i class="fa fa-trash"></i>
</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -229,12 +256,5 @@ $(function(){
active: false active: false
}); });
}); });
$(function(){
$("#drop_subscriptions").accordion({
heightStyle: "content",
collapsible: true,
active: false
});
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -23,11 +23,13 @@
# #
# #
import datetime
import phonenumbers import phonenumbers
from django import template from django import template
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ngettext
from core.scss.processor import ScssProcessor from core.scss.processor import ScssProcessor
from core.markdown import markdown as md from core.markdown import markdown as md
@ -54,41 +56,26 @@ def phonenumber(value, country="FR", format=phonenumbers.PhoneNumberFormat.NATIO
return value return value
@register.filter() @register.filter(name="truncate_time")
@stringfilter def truncate_time(value, time_unit):
def datetime_format_python_to_PHP(python_format_string): value = str(value)
""" return {
Given a python datetime format string, attempts to convert it to the nearest PHP datetime format string possible. "millis": lambda: value.split(".")[0],
""" "seconds": lambda: value.rsplit(":", maxsplit=1)[0],
python2PHP = { "minutes": lambda: value.split(":", maxsplit=1)[0],
"%a": "D", "hours": lambda: value.rsplit(" ")[0],
"%a": "D", }[time_unit]()
"%A": "l",
"%b": "M",
"%B": "F",
"%c": "",
"%d": "d",
"%H": "H",
"%I": "h",
"%j": "z",
"%m": "m",
"%M": "i",
"%p": "A",
"%S": "s",
"%U": "",
"%w": "w",
"%W": "W",
"%x": "",
"%X": "",
"%y": "y",
"%Y": "Y",
"%Z": "e",
}
php_format_string = python_format_string
for py, php in python2PHP.items(): @register.filter(name="format_timedelta")
php_format_string = php_format_string.replace(py, php) def format_timedelta(value: datetime.timedelta) -> str:
return php_format_string days = value.days
if days == 0:
return str(value)
remainder = value - datetime.timedelta(days=days)
return ngettext(
"%(nb_days)d day, %(remainder)s", "%(nb_days)d days, %(remainder)s", days
) % {"nb_days": days, "remainder": str(remainder)}
@register.simple_tag() @register.simple_tag()

View File

@ -31,7 +31,6 @@ from datetime import date
from PIL import ExifTags from PIL import ExifTags
# from exceptions import IOError
import PIL import PIL
from django.conf import settings from django.conf import settings
@ -52,14 +51,12 @@ def get_start_of_semester(d=date.today()):
year = today.year year = today.year
start = date(year, settings.SITH_START_DATE[0], settings.SITH_START_DATE[1]) start = date(year, settings.SITH_START_DATE[0], settings.SITH_START_DATE[1])
start2 = start.replace(month=(start.month + 6) % 12) start2 = start.replace(month=(start.month + 6) % 12)
if start > start2: spring, autumn = min(start, start2), max(start, start2)
start, start2 = start2, start if today > autumn: # autumn semester
if today < start: return autumn
return start2.replace(year=year - 1) if today > spring: # spring semester
elif today < start2: return spring
return start return autumn.replace(year=year - 1) # autumn semester of last year
else:
return start2
def get_semester(d=date.today()): def get_semester(d=date.today()):

View File

@ -22,13 +22,12 @@
# #
# #
from __future__ import annotations from __future__ import annotations
from django.db.models import Sum, F
from typing import Tuple from typing import Tuple
from django.db import models from django.db import models
from django.db.models import OuterRef, Exists from django.db.models import F, Value, Sum, QuerySet, OuterRef, Exists
from django.db.models.functions import Length from django.db.models.functions import Concat, Length
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
@ -37,14 +36,14 @@ from django.core.validators import MinLengthValidator
from django.forms import ValidationError from django.forms import ValidationError
from django.utils.functional import cached_property from django.utils.functional import cached_property
from datetime import timedelta, date from datetime import timedelta, date, datetime
import random import random
import string import string
import os import os
import base64 import base64
import datetime
from dict2xml import dict2xml from dict2xml import dict2xml
from core.utils import get_start_of_semester
from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB
from club.models import Club, Membership from club.models import Club, Membership
from accounting.models import CurrencyField from accounting.models import CurrencyField
@ -92,8 +91,9 @@ class Customer(models.Model):
don't mix them) and a Product. don't mix them) and a Product.
""" """
subscription = self.user.subscriptions.order_by("subscription_end").last() subscription = self.user.subscriptions.order_by("subscription_end").last()
time_diff = date.today() - subscription.subscription_end if subscription is None:
return subscription is not None and time_diff < timedelta(days=90) return False
return (date.today() - subscription.subscription_end) < timedelta(days=90)
@classmethod @classmethod
def get_or_create(cls, user: User) -> Tuple[Customer, bool]: def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
@ -491,7 +491,7 @@ class Counter(models.Model):
""" """
return self.is_open() and ( return self.is_open() and (
(timezone.now() - self.permanencies.order_by("-activity").first().activity) (timezone.now() - self.permanencies.order_by("-activity").first().activity)
> datetime.timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE) > timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
) )
def barman_list(self): def barman_list(self):
@ -517,6 +517,77 @@ class Counter(models.Model):
is_ae_member = True is_ae_member = True
return is_ae_member return is_ae_member
def get_top_barmen(self) -> QuerySet:
"""
Return a QuerySet querying the office hours stats of all the barmen of all time
of this counter, ordered by descending number of hours.
Each element of the QuerySet corresponds to a barman and has the following data :
- the full name (first name + last name) of the barman
- the nickname of the barman
- the promo of the barman
- the total number of office hours the barman did attend
"""
return (
self.permanencies.exclude(end=None)
.annotate(
name=Concat(F("user__first_name"), Value(" "), F("user__last_name"))
)
.annotate(nickname=F("user__nick_name"))
.annotate(promo=F("user__promo"))
.values("user", "name", "nickname", "promo")
.annotate(perm_sum=Sum(F("end") - F("start")))
.exclude(perm_sum=None)
.order_by("-perm_sum")
)
def get_top_customers(self, since=get_start_of_semester()) -> QuerySet:
"""
Return a QuerySet querying the money spent by customers of this counter
since the specified date, ordered by descending amount of money spent.
Each element of the QuerySet corresponds to a customer and has the following data :
- the full name (first name + last name) of the customer
- the nickname of the customer
- the amount of money spent by the customer
"""
return (
self.sellings.filter(date__gte=since)
.annotate(
name=Concat(
F("customer__user__first_name"),
Value(" "),
F("customer__user__last_name"),
)
)
.annotate(nickname=F("customer__user__nick_name"))
.annotate(promo=F("customer__user__promo"))
.values("customer__user", "name", "nickname")
.annotate(
selling_sum=Sum(
F("unit_price") * F("quantity"), output_field=CurrencyField()
)
)
.filter(selling_sum__gt=0)
.order_by("-selling_sum")
)
def get_total_sales(self, since=get_start_of_semester()) -> CurrencyField:
"""
Compute and return the total turnover of this counter
since the date specified in parameter (by default, since the start of the current
semester)
:param since: timestamp from which to perform the calculation
:type since: datetime | date
:return: Total revenue earned at this counter
"""
if isinstance(since, date):
since = datetime.combine(since, datetime.min.time())
total = self.sellings.filter(date__gte=since).aggregate(
total=Sum(F("quantity") * F("unit_price"), output_field=CurrencyField())
)["total"]
return total if total is not None else CurrencyField(0)
class Refilling(models.Model): class Refilling(models.Model):
""" """

View File

@ -5,40 +5,31 @@
{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %} {% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}
{% endblock %} {% endblock %}
{% block jquery_css %}
{# Remove jquery_css #}
{% endblock %}
{% block content %} {% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}</h3> <h3>{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}</h3>
<h4>{% trans counter_name=counter.name %}Top 100 {{ counter_name }}{% endtrans %}</h4> <h4>{% trans counter_name=counter.name %}Top 100 {{ counter_name }}{% endtrans %}</h4>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Nb{% endtrans %}</td> <td>N°</td>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Promo{% endtrans %}</td> <td>{% trans %}Promo{% endtrans %}</td>
<td>{% trans %}Clubs{% endtrans %}</td>
<td>{% trans %}Total{% endtrans %}</td> <td>{% trans %}Total{% endtrans %}</td>
<td>{% trans %}Percentage{% endtrans %}</td> <td>{% trans %}Percentage{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in top %} {% for customer in top_customers %}
{% set customer=Customer.objects.filter(user__id=r.customer__user).first() %} <tr class="{% if customer.user == request.user.id %}highlight{% endif %}">
{% if customer.user == user %}
<tr class="highlight">
{% else %}
<tr>
{% endif %}
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td>{{ customer.user.get_display_name() }}</td> <td>{{ customer.name }} {% if customer.nickname %} ({{ customer.nickname }}) {% endif %}</td>
<td>{{ customer.user.promo or '' }}</td> <td>{{ customer.promo or '' }}</td>
<td> <td>{{ "%.2f"|format(customer.selling_sum) }} €</td>
{% for m in customer.user.memberships.filter(club__parent=None, end_date=None, <td>{{ '%.2f'|format(100 * customer.selling_sum / total_sellings) }}%</td>
role__gt=settings.SITH_MAXIMUM_FREE_ROLE).all() -%}
{%- if loop.index>1 -%}, {% endif -%}
{{ m.club.name }}
{%- endfor %}
</td>
<td>{{ r.selling_sum }} €</td>
<td>{{ '%.2f'|format(100 * r.selling_sum / total_sellings) }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -48,22 +39,19 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Nb{% endtrans %}</td> <td>N°</td>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Promo{% endtrans %}</td>
<td>{% trans %}Time{% endtrans %}</td> <td>{% trans %}Time{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in top_barman_semester %} {% for barman in top_barman_semester %}
{% set u=User.objects.filter(id=r.user).first() %} <tr {% if barman.user == request.user.id %}class="highlight"{% endif %}>
{% if u == user %}
<tr class="highlight">
{% else %}
<tr>
{% endif %}
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td>{{ u.get_display_name() }}</td> <td>{{ barman.name }} {% if barman.nickname %}({{ barman.nickname }}){% endif %}</td>
<td>{{ r.perm_sum }}</td> <td>{{ barman.promo or '' }}</td>
<td>{{ barman.perm_sum|format_timedelta|truncate_time("millis") }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -73,22 +61,19 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Nb{% endtrans %}</td> <td>N°</td>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Promo{% endtrans %}</td>
<td>{% trans %}Time{% endtrans %}</td> <td>{% trans %}Time{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in top_barman %} {% for barman in top_barman %}
{% set u=User.objects.filter(id=r.user).first() %} <tr {% if barman.user == request.user.id %}class="highlight"{% endif %}>
{% if u == user %}
<tr class="highlight">
{% else %}
<tr>
{% endif %}
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td>{{ u.get_display_name() }}</td> <td>{{ barman.name }} {% if barman.nickname %}({{ barman.nickname }}){% endif %}</td>
<td>{{ r.perm_sum }}</td> <td>{{ barman.promo or '' }}</td>
<td>{{ barman.perm_sum|format_timedelta|truncate_time("millis") }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -28,9 +28,13 @@ import string
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.core.management import call_command from django.core.management import call_command
from django.utils import timezone
from django.utils.timezone import timedelta
from club.models import Club
from core.models import User from core.models import User
from counter.models import Counter, Customer, BillingInfo from counter.models import Counter, Customer, BillingInfo, Permanency, Selling, Product
from sith.settings import SITH_MAIN_CLUB
class CounterTest(TestCase): class CounterTest(TestCase):
@ -164,15 +168,211 @@ class CounterTest(TestCase):
class CounterStatsTest(TestCase): class CounterStatsTest(TestCase):
def setUp(self): @classmethod
def setUpClass(cls):
super().setUpClass()
call_command("populate") call_command("populate")
self.counter = Counter.objects.filter(id=2).first() cls.counter = Counter.objects.filter(id=2).first()
cls.krophil = User.objects.get(username="krophil")
cls.skia = User.objects.get(username="skia")
cls.sli = User.objects.get(username="sli")
cls.root = User.objects.get(username="root")
cls.subscriber = User.objects.get(username="subscriber")
cls.old_subscriber = User.objects.get(username="old_subscriber")
cls.counter.sellers.add(cls.sli)
cls.counter.sellers.add(cls.root)
cls.counter.sellers.add(cls.skia)
cls.counter.sellers.add(cls.krophil)
def test_unauthorised_user_fail(self): barbar = Product.objects.get(code="BARB")
# remove everything to make sure the fixtures bring no side effect
Permanency.objects.all().delete()
Selling.objects.all().delete()
now = timezone.now()
# total of sli : 5 hours
Permanency.objects.create(
user=cls.sli, start=now, end=now + timedelta(hours=1), counter=cls.counter
)
Permanency.objects.create(
user=cls.sli,
start=now + timedelta(hours=4),
end=now + timedelta(hours=6),
counter=cls.counter,
)
Permanency.objects.create(
user=cls.sli,
start=now + timedelta(hours=7),
end=now + timedelta(hours=9),
counter=cls.counter,
)
# total of skia : 16 days, 2 hours, 35 minutes and 54 seconds
Permanency.objects.create(
user=cls.skia, start=now, end=now + timedelta(hours=1), counter=cls.counter
)
Permanency.objects.create(
user=cls.skia,
start=now + timedelta(days=4, hours=1),
end=now + timedelta(days=20, hours=2, minutes=35, seconds=54),
counter=cls.counter,
)
# total of root : 1 hour + 20 hours (but the 20 hours were on last year)
Permanency.objects.create(
user=cls.root,
start=now + timedelta(days=5),
end=now + timedelta(days=5, hours=1),
counter=cls.counter,
)
Permanency.objects.create(
user=cls.root,
start=now - timedelta(days=300, hours=20),
end=now - timedelta(days=300),
counter=cls.counter,
)
# total of krophil : 0 hour
s = Selling(
label=barbar.name,
product=barbar,
club=Club.objects.get(name=SITH_MAIN_CLUB["name"]),
counter=cls.counter,
unit_price=2,
seller=cls.skia,
)
krophil_customer = Customer.get_or_create(cls.krophil)[0]
sli_customer = Customer.get_or_create(cls.sli)[0]
skia_customer = Customer.get_or_create(cls.skia)[0]
root_customer = Customer.get_or_create(cls.root)[0]
# moderate drinker. Total : 100 €
s.quantity = 50
s.customer = krophil_customer
s.save(allow_negative=True)
# Sli is a drunkard. Total : 2000 €
s.quantity = 100
s.customer = sli_customer
for _ in range(10):
# little trick to make sure the instance is duplicated in db
s.pk = None
s.save(allow_negative=True) # save ten different sales
# Skia is a heavy drinker too. Total : 1000 €
s.customer = skia_customer
for _ in range(5):
s.pk = None
s.save(allow_negative=True)
# Root is quite an abstemious one. Total : 2 €
s.pk = None
s.quantity = 1
s.customer = root_customer
s.save(allow_negative=True)
def test_not_authenticated_user_fail(self):
# Test with not login user # Test with not login user
response = self.client.get(reverse("counter:stats", args=[self.counter.id])) response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
self.assertTrue(response.status_code == 403) self.assertTrue(response.status_code == 403)
def test_unauthorized_user_fails(self):
user = User.objects.get(username="public")
self.client.login(username=user.username, password="plop")
response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
self.assertTrue(response.status_code == 403)
def test_get_total_sales(self):
"""
Test the result of the Counter.get_total_sales() method
"""
total = self.counter.get_total_sales()
self.assertEqual(total, 3102)
def test_top_barmen(self):
"""
Test the result of Counter.get_top_barmen() is correct
"""
top = iter(self.counter.get_top_barmen())
self.assertEqual(
next(top),
{
"user": self.skia.id,
"name": f"{self.skia.first_name} {self.skia.last_name}",
"promo": self.skia.promo,
"nickname": self.skia.nick_name,
"perm_sum": timedelta(days=16, hours=2, minutes=35, seconds=54),
},
)
self.assertEqual(
next(top),
{
"user": self.root.id,
"name": f"{self.root.first_name} {self.root.last_name}",
"promo": self.root.promo,
"nickname": self.root.nick_name,
"perm_sum": timedelta(hours=21),
},
)
self.assertEqual(
next(top),
{
"user": self.sli.id,
"name": f"{self.sli.first_name} {self.sli.last_name}",
"promo": self.sli.promo,
"nickname": self.sli.nick_name,
"perm_sum": timedelta(hours=5),
},
)
self.assertIsNone(
next(top, None), msg="barmen with no office hours should not be in the top"
)
def test_top_customer(self):
"""
Test the result of Counter.get_top_customers() is correct
"""
top = iter(self.counter.get_top_customers())
self.assertEqual(
next(top),
{
"customer__user": self.sli.id,
"name": f"{self.sli.first_name} {self.sli.last_name}",
"nickname": self.sli.nick_name,
"selling_sum": 2000,
},
)
self.assertEqual(
next(top),
{
"customer__user": self.skia.id,
"name": f"{self.skia.first_name} {self.skia.last_name}",
"nickname": self.skia.nick_name,
"selling_sum": 1000,
},
)
self.assertEqual(
next(top),
{
"customer__user": self.krophil.id,
"name": f"{self.krophil.first_name} {self.krophil.last_name}",
"nickname": self.krophil.nick_name,
"selling_sum": 100,
},
)
self.assertEqual(
next(top),
{
"customer__user": self.root.id,
"name": f"{self.root.first_name} {self.root.last_name}",
"nickname": self.root.nick_name,
"selling_sum": 2,
},
)
self.assertIsNone(next(top, None))
class BillingInfoTest(TestCase): class BillingInfoTest(TestCase):
@classmethod @classmethod

View File

@ -48,13 +48,15 @@ from django.utils import timezone
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.db import DataError, transaction, models from django.db import DataError, transaction
import json
import re import re
import pytz import pytz
from datetime import date, timedelta, datetime from datetime import timedelta, datetime
from http import HTTPStatus from http import HTTPStatus
from core.utils import get_start_of_semester
from core.views import CanViewMixin, TabedViewMixin, CanEditMixin from core.views import CanViewMixin, TabedViewMixin, CanEditMixin
from core.views.forms import LoginForm from core.views.forms import LoginForm
from core.models import User from core.models import User
@ -68,7 +70,6 @@ from counter.forms import (
CashSummaryFormBase, CashSummaryFormBase,
EticketForm, EticketForm,
) )
from subscription.models import Subscription
from counter.models import ( from counter.models import (
Counter, Counter,
Customer, Customer,
@ -80,7 +81,6 @@ from counter.models import (
CashRegisterSummary, CashRegisterSummary,
CashRegisterSummaryItem, CashRegisterSummaryItem,
Eticket, Eticket,
Permanency,
BillingInfo, BillingInfo,
) )
from accounting.models import CurrencyField from accounting.models import CurrencyField
@ -1365,80 +1365,21 @@ class CounterStatView(DetailView, CounterAdminMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add stats to the context""" """Add stats to the context"""
from django.db.models import Sum, Case, When, F counter = self.object
semester_start = get_start_of_semester()
office_hours = counter.get_top_barmen()
kwargs = super(CounterStatView, self).get_context_data(**kwargs) kwargs = super(CounterStatView, self).get_context_data(**kwargs)
kwargs["Customer"] = Customer kwargs.update(
kwargs["User"] = User {
semester_start = Subscription.compute_start(d=date.today(), duration=3) "counter": counter,
kwargs["total_sellings"] = Selling.objects.filter( "total_sellings": counter.get_total_sales(since=semester_start),
date__gte=semester_start, counter=self.object "top_customers": counter.get_top_customers(since=semester_start)[:100],
).aggregate( "top_barman": office_hours[:100],
total_sellings=Sum( "top_barman_semester": (
F("quantity") * F("unit_price"), output_field=CurrencyField() office_hours.filter(start__gt=semester_start)[:100]
)
)[
"total_sellings"
]
kwargs["top"] = (
Selling.objects.values("customer__user")
.annotate(
selling_sum=Sum(
Case(
When(
counter=self.object,
date__gte=semester_start,
unit_price__gt=0,
then=F("unit_price") * F("quantity"),
), ),
output_field=CurrencyField(), }
) )
)
)
.exclude(selling_sum=None)
.order_by("-selling_sum")
.all()[:100]
)
kwargs["top_barman"] = (
Permanency.objects.values("user")
.annotate(
perm_sum=Sum(
Case(
When(
counter=self.object,
end__gt=datetime(year=1999, month=1, day=1),
then=F("end") - F("start"),
),
output_field=models.DateTimeField(),
)
)
)
.exclude(perm_sum=None)
.order_by("-perm_sum")
.all()[:100]
)
kwargs["top_barman_semester"] = (
Permanency.objects.values("user")
.annotate(
perm_sum=Sum(
Case(
When(
counter=self.object,
start__gt=semester_start,
end__gt=datetime(year=1999, month=1, day=1),
then=F("end") - F("start"),
),
output_field=models.DateTimeField(),
)
)
)
.exclude(perm_sum=None)
.order_by("-perm_sum")
.all()[:100]
)
kwargs["sith_date"] = settings.SITH_START_DATE[0]
kwargs["semester_start"] = semester_start
return kwargs return kwargs
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):

File diff suppressed because it is too large Load Diff

View File

@ -147,6 +147,8 @@ TEMPLATES = [
"filters": { "filters": {
"markdown": "core.templatetags.renderer.markdown", "markdown": "core.templatetags.renderer.markdown",
"phonenumber": "core.templatetags.renderer.phonenumber", "phonenumber": "core.templatetags.renderer.phonenumber",
"truncate_time": "core.templatetags.renderer.truncate_time",
"format_timedelta": "core.templatetags.renderer.format_timedelta",
}, },
"globals": { "globals": {
"can_edit_prop": "core.views.can_edit_prop", "can_edit_prop": "core.views.can_edit_prop",