mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 14:13:21 +00:00
remove-useless-queries-counter-stats (#519)
This commit is contained in:
parent
f0a08afd31
commit
6c1fa6de0b
@ -1198,18 +1198,38 @@ blockquote h5:first-child {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 90%;
|
||||||
font-size: 0.9em;
|
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 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -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,24 +150,35 @@
|
|||||||
</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">
|
||||||
<table>
|
<span class="collapse-header-text">
|
||||||
<tr>
|
{% trans %}Subscription history{% endtrans %}
|
||||||
<th>{% trans %}Subscription start{% endtrans %}</th>
|
</span>
|
||||||
<th>{% trans %}Subscription end{% endtrans %}</th>
|
<span class="collapse-header-icon" :class="{'reverse': collapsed}">
|
||||||
<th>{% trans %}Subscription type{% endtrans %}</th>
|
<i class="fa fa-caret-down"></i>
|
||||||
<th>{% trans %}Payment method{% endtrans %}</th>
|
</span>
|
||||||
</tr>
|
</div>
|
||||||
{% for sub in profile.subscriptions.all() %}
|
<div class="collapse-body" x-show="collapsed" x-transition.scale.origin.top>
|
||||||
<tr>
|
<table>
|
||||||
<td>{{ sub.subscription_start }}</td>
|
<thead>
|
||||||
<td>{{ sub.subscription_end }}</td>
|
<tr>
|
||||||
<td>{{ sub.subscription_type }}</td>
|
<th>{% trans %}Subscription start{% endtrans %}</th>
|
||||||
<td>{{ sub.get_payment_method_display() }}</td>
|
<th>{% trans %}Subscription end{% endtrans %}</th>
|
||||||
</tr>
|
<th>{% trans %}Subscription type{% endtrans %}</th>
|
||||||
{% endfor %}
|
<th>{% trans %}Payment method{% endtrans %}</th>
|
||||||
</table>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for sub in profile.subscriptions.all() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ sub.subscription_start }}</td>
|
||||||
|
<td>{{ sub.subscription_end }}</td>
|
||||||
|
<td>{{ sub.subscription_type }}</td>
|
||||||
|
<td>{{ sub.get_payment_method_display() }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -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() %}
|
||||||
<br>
|
{% set gifts = profile.gifts.order_by("-date")|list %}
|
||||||
<div id="drop_gifts">
|
<br>
|
||||||
<h5>{% trans %}Last given gift :{% endtrans %} {{ profile.gifts.order_by('-date').first() }}</h5>
|
<div class="collapse" :class="{'shadow': collapsed}" x-data="{collapsed: false}" x-cloak>
|
||||||
<div>
|
<div class="collapse-header clickable" @click="collapsed = !collapsed">
|
||||||
|
<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 %}
|
||||||
|
@ -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()
|
||||||
|
@ -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()):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -2,43 +2,34 @@
|
|||||||
{% from 'core/macros.jinja' import user_profile_link %}
|
{% from 'core/macros.jinja' import user_profile_link %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}
|
{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block jquery_css %}
|
||||||
|
{# Remove jquery_css #}
|
||||||
{% endblock %}
|
{% 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>
|
||||||
@ -47,23 +38,20 @@
|
|||||||
<h4>{% trans counter_name=counter.name %}Top 100 barman {{ counter_name }}{% endtrans %}</h4>
|
<h4>{% trans counter_name=counter.name %}Top 100 barman {{ 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 %}Time{% endtrans %}</td>
|
<td>{% trans %}Promo{% endtrans %}</td>
|
||||||
</tr>
|
<td>{% trans %}Time{% endtrans %}</td>
|
||||||
|
</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>
|
||||||
@ -72,23 +60,20 @@
|
|||||||
<h4>{% trans counter_name=counter.name %}Top 100 barman {{ counter_name }} (all semesters){% endtrans %}</h4>
|
<h4>{% trans counter_name=counter.name %}Top 100 barman {{ counter_name }} (all semesters){% 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 %}Time{% endtrans %}</td>
|
<td>{% trans %}Promo{% endtrans %}</td>
|
||||||
</tr>
|
<td>{% trans %}Time{% endtrans %}</td>
|
||||||
|
</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>
|
||||||
|
208
counter/tests.py
208
counter/tests.py
@ -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
|
||||||
|
@ -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
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user