Optimize user account pages

This commit is contained in:
imperosol 2024-10-04 13:41:15 +02:00
parent f6be360eab
commit 58d3a7ee2c
5 changed files with 208 additions and 137 deletions

View File

@ -1,6 +1,6 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% macro monthly(obj) %} {% macro monthly(objects) %}
<div> <div>
<table> <table>
<thead> <thead>
@ -11,17 +11,18 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for array in obj %} {% for object in objects %}
{% for dict in array %} {% set link=url(
{% if dict['sum'] != 0 %} 'core:user_account_detail',
{% set link=url('core:user_account_detail', user_id=profile.id, year=dict['date'].year, month=dict['date'].month) %} user_id=profile.id,
year=object['grouped_date'].year,
month=object['grouped_date'].month
) %}
<tr> <tr>
<td><a href="{{ link }}">{{ dict['date'].year }}</a></td> <td><a href="{{ link }}">{{ object["grouped_date"]|date("Y") }}</a></td>
<td><a href="{{ link }}">{{ dict['date']|date("E") }}</a></td> <td><a href="{{ link }}">{{ object["grouped_date"]|date("E") }}</a></td>
<td><a href="{{ link }}">{{ dict['sum'] }} €</a></td> <td><a href="{{ link }}">{{ "%.2f"|format(object["total"]) }} €</a></td>
</tr> </tr>
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -37,19 +38,15 @@
<h3>{% trans %}User account{% endtrans %}</h3> <h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<div id="drop"> <div id="drop">
{% set bought = customer.buyings.exists() %} {% if buyings_month %}
{% set refilled = customer.refillings.exists() %}
{% if bought or refilled %}
{% if bought %}
<h5>{% trans %}Account purchases{% endtrans %}</h5> <h5>{% trans %}Account purchases{% endtrans %}</h5>
{{ monthly(buyings_month) }} {{ monthly(buyings_month) }}
{% endif %} {% endif %}
{% if refilled %} {% if refilling_month %}
<h5>{% trans %}Reloads{% endtrans %}</h5> <h5>{% trans %}Reloads{% endtrans %}</h5>
{{ monthly(refilling_month) }} {{ monthly(refilling_month) }}
{% endif %} {% endif %}
{% endif %} {% if invoices_month %}
{% if customer.user.invoices.exists() %}
<h5>{% trans %}Eboutic invoices{% endtrans %}</h5> <h5>{% trans %}Eboutic invoices{% endtrans %}</h5>
{{ monthly(invoices_month) }} {{ monthly(invoices_month) }}
{% endif %} {% endif %}
@ -58,7 +55,11 @@
<div> <div>
<ul> <ul>
{% for s in etickets %} {% for s in etickets %}
<li><a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">{{ s.quantity }} x {{ s.product.eticket }}</a></li> <li>
<a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">
{{ s.quantity }} x {{ s.product.eticket }}
</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -5,11 +5,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if customer %}
<h3>{% trans %}User account{% endtrans %}</h3> <h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p> <p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% if customer.buyings.exists() %} {% if purchases %}
<h4>{% trans %}Account purchases{% endtrans %}</h4> <h4>{% trans %}Account purchases{% endtrans %}</h4>
<table> <table>
<thead> <thead>
@ -24,25 +23,31 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i in customer.buyings.order_by('-date').all().filter( {% for purchase in purchases %}
date__year=year, date__month=month) %}
<tr> <tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td> <td>
<td>{{ i.counter }}</td> {{ purchase.date|localtime|date(DATETIME_FORMAT) }}
<td><a href="{{ i.seller.get_absolute_url() }}">{{ i.seller.get_display_name() }}</a></td> - {{ purchase.date|localtime|time(DATETIME_FORMAT) }}
<td>{{ i.label }}</td> </td>
<td>{{ i.quantity }}</td> <td>{{ purchase.counter }}</td>
<td>{{ i.quantity * i.unit_price }} €</td> <td><a href="{{ purchase.seller.get_absolute_url() }}">{{ purchase.seller.get_display_name() }}</a></td>
<td>{{ i.get_payment_method_display() }}</td> <td>{{ purchase.label }}</td>
{% if i.is_owned_by(user) %} <td>{{ purchase.quantity }}</td>
<td><a href="{{ url('counter:selling_delete', selling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td> <td>{{ purchase.quantity * purchase.unit_price }} €</td>
<td>{{ purchase.get_payment_method_display() }}</td>
{% if purchase.is_owned_by(user) %}
<td>
<a href="{{ url('counter:selling_delete', selling_id=purchase.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if customer.refillings.exists() %} {% if refills %}
<h4>{% trans %}Reloads{% endtrans %}</h4> <h4>{% trans %}Reloads{% endtrans %}</h4>
<table> <table>
<thead> <thead>
@ -55,22 +60,30 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i in customer.refillings.order_by('-date').filter( date__year=year, date__month=month) %} {% for refill in refills %}
<tr> <tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td> <td>{{ refill.date|localtime|date(DATETIME_FORMAT) }} - {{ refill.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ i.counter }}</td> <td>{{ refill.counter }}</td>
<td><a href="{{ i.operator.get_absolute_url() }}">{{ i.operator.get_display_name() }}</a></td> <td>
<td>{{ i.amount }} €</td> <a href="{{ refill.operator.get_absolute_url() }}">
<td>{{ i.get_payment_method_display() }}</td> {{ refill.operator.get_display_name() }}
{% if i.is_owned_by(user) %} </a>
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td> </td>
<td>{{ refill.amount }} €</td>
<td>{{ refill.get_payment_method_display() }}</td>
{% if refill.is_owned_by(user) %}
<td>
<a href="{{ url('counter:refilling_delete', refilling_id=refill.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if customer.user.invoices.exists() %} {% if invoices %}
<h4>{% trans %}Eboutic invoices{% endtrans %}</h4> <h4>{% trans %}Eboutic invoices{% endtrans %}</h4>
<table> <table>
<thead> <thead>
@ -81,25 +94,24 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i in customer.user.invoices.order_by('-date').all().filter( {% for invoice in invoices %}
date__year=year, date__month=month) %}
<tr> <tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td> <td>
{{ invoice.date|localtime|date(DATETIME_FORMAT) }}
- {{ invoice.date|localtime|time(DATETIME_FORMAT) }}
</td>
<td> <td>
<ul> <ul>
{% for it in i.items.all() %} {% for it in invoice.items.all() %}
<li>{{ it.quantity }} x {{ it.product_name }} - {{ it.product_unit_price }} €</li> <li>{{ it.quantity }} x {{ it.product_name }} - {{ it.product_unit_price }} €</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td>{{ i.get_total() }} €</td> <td>{{ invoice.total }} €</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% else %}
<p>{% trans %}User has no account{% endtrans %}</p>
{% endif %}
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p> <p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% endblock %} {% endblock %}

View File

@ -21,7 +21,6 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
import logging
# This file contains all the views that concern the user model # This file contains all the views that concern the user model
from datetime import date, timedelta from datetime import date, timedelta
@ -32,6 +31,8 @@ from django.contrib.auth import login, views
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import DateField, QuerySet
from django.db.models.functions import Trunc
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.http import Http404 from django.http import Http404
@ -68,6 +69,8 @@ from core.views.forms import (
UserProfileForm, UserProfileForm,
) )
from counter.forms import StudentCardForm from counter.forms import StudentCardForm
from counter.models import Refilling, Selling
from eboutic.models import Invoice
from subscription.models import Subscription from subscription.models import Subscription
from trombi.views import UserTrombiForm from trombi.views import UserTrombiForm
@ -615,18 +618,18 @@ class UserAccountBase(UserTabsMixin, DetailView):
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
current_tab = "account" current_tab = "account"
queryset = User.objects.select_related("customer")
def dispatch(self, request, *arg, **kwargs): # Manually validates the rights def dispatch(self, request, *arg, **kwargs): # Manually validates the rights
res = super().dispatch(request, *arg, **kwargs)
if ( if (
self.object == request.user kwargs.get("user_id") == request.user.id
or request.user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) or request.user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or request.user.is_in_group( or request.user.is_in_group(
name=settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX name=settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
) )
or request.user.is_root or request.user.is_root
): ):
return res return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied raise PermissionDenied
@ -635,45 +638,31 @@ class UserAccountView(UserAccountBase):
template_name = "core/user_account.jinja" template_name = "core/user_account.jinja"
def expense_by_month(self, obj, calc): @staticmethod
stats = [] def expense_by_month[T](qs: QuerySet[T]) -> QuerySet[T]:
month_trunc = Trunc("date", "month", output_field=DateField())
for year in obj.datetimes("date", "year", order="DESC"): return (
stats.append([]) qs.annotate(grouped_date=month_trunc)
i = 0 .values("grouped_date")
for month in obj.filter(date__year=year.year).datetimes( .annotate_total()
"date", "month", order="DESC" .exclude(total=0)
): .order_by("-grouped_date")
q = obj.filter(date__year=month.year, date__month=month.month) )
stats[i].append({"sum": sum([calc(p) for p in q]), "date": month})
i += 1
return stats
def invoices_calc(self, query):
t = 0
for it in query.items.all():
t += it.quantity * it.product_unit_price
return t
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["profile"] = self.object kwargs["profile"] = self.object
try:
kwargs["customer"] = self.object.customer kwargs["customer"] = self.object.customer
kwargs["buyings_month"] = self.expense_by_month( kwargs["buyings_month"] = self.expense_by_month(
self.object.customer.buyings, (lambda q: q.unit_price * q.quantity) Selling.objects.filter(customer=self.object.customer)
)
kwargs["invoices_month"] = self.expense_by_month(
self.object.customer.user.invoices, self.invoices_calc
) )
kwargs["refilling_month"] = self.expense_by_month( kwargs["refilling_month"] = self.expense_by_month(
self.object.customer.refillings, (lambda q: q.amount) Refilling.objects.filter(customer=self.object.customer)
) )
kwargs["etickets"] = self.object.customer.buyings.exclude( kwargs["invoices_month"] = self.expense_by_month(
product__eticket=None Invoice.objects.filter(user=self.object)
).all() )
except Exception as e: kwargs["etickets"] = self.object.customer.buyings.exclude(product__eticket=None)
logging.error(e)
return kwargs return kwargs
@ -682,16 +671,37 @@ class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
template_name = "core/user_account_detail.jinja" template_name = "core/user_account_detail.jinja"
def get(self, request, *args, **kwargs):
if not hasattr(self.get_object(), "customer"):
raise Http404(_("This user has no account"))
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["profile"] = self.object kwargs["profile"] = self.object
kwargs["year"] = self.get_year()
kwargs["month"] = self.get_month()
try:
kwargs["customer"] = self.object.customer kwargs["customer"] = self.object.customer
except: year, month = self.get_year(), self.get_month()
pass filters = {
kwargs["tab"] = "account" "customer": self.object.customer,
"date__year": year,
"date__month": month,
}
kwargs["purchases"] = list(
Selling.objects.filter(**filters)
.select_related("counter", "counter__club", "seller")
.order_by("-date")
)
kwargs["refills"] = list(
Refilling.objects.filter(**filters)
.select_related("counter", "counter__club", "operator")
.order_by("-date")
)
kwargs["invoices"] = list(
Invoice.objects.filter(user=self.object, date__year=year, date__month=month)
.annotate_total()
.prefetch_related("items")
.order_by("-date")
)
return kwargs return kwargs

View File

@ -20,7 +20,7 @@ import random
import string import string
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from datetime import timezone as tz from datetime import timezone as tz
from typing import Tuple from typing import Self, Tuple
from dict2xml import dict2xml from dict2xml import dict2xml
from django.conf import settings from django.conf import settings
@ -585,6 +585,23 @@ class Counter(models.Model):
)["total"] )["total"]
class RefillingQuerySet(models.QuerySet):
def annotate_total(self) -> Self:
"""Annotate the Queryset with the total amount.
The total is just the sum of the amounts for each row.
If no grouping is involved (like in most queries),
this is just the same as doing nothing and fetching the
`amount` attribute.
However, it may be useful when there is a `group by` clause
in the query, or when other models are queried and having
a common interface is helpful (e.g. `Selling.objects.annotate_total()`
and `Refilling.objects.annotate_total()` will both have the `total` field).
"""
return self.annotate(total=Sum("amount"))
class Refilling(models.Model): class Refilling(models.Model):
"""Handle the refilling.""" """Handle the refilling."""
@ -613,6 +630,8 @@ class Refilling(models.Model):
) )
is_validated = models.BooleanField(_("is validated"), default=False) is_validated = models.BooleanField(_("is validated"), default=False)
objects = RefillingQuerySet.as_manager()
class Meta: class Meta:
verbose_name = _("refilling") verbose_name = _("refilling")
@ -657,6 +676,15 @@ class Refilling(models.Model):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
class SellingQuerySet(models.QuerySet):
def annotate_total(self) -> Self:
"""Annotate the Queryset with the total amount of the sales.
The total is considered as the sum of (unit_price * quantity).
"""
return self.annotate(total=Sum(F("unit_price") * F("quantity")))
class Selling(models.Model): class Selling(models.Model):
"""Handle the sellings.""" """Handle the sellings."""
@ -703,6 +731,8 @@ class Selling(models.Model):
) )
is_validated = models.BooleanField(_("is validated"), default=False) is_validated = models.BooleanField(_("is validated"), default=False)
objects = SellingQuerySet.as_manager()
class Meta: class Meta:
verbose_name = _("selling") verbose_name = _("selling")

View File

@ -16,12 +16,12 @@ from __future__ import annotations
import hmac import hmac
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any, Self
from dict2xml import dict2xml from dict2xml import dict2xml
from django.conf import settings from django.conf import settings
from django.db import DataError, models from django.db import DataError, models
from django.db.models import F, Sum from django.db.models import F, OuterRef, Subquery, Sum
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -160,6 +160,22 @@ class Basket(models.Model):
return data return data
class InvoiceQueryset(models.QuerySet):
def annotate_total(self) -> Self:
"""Annotate the queryset with the total amount of each invoice.
The total amount is the sum of (product_unit_price * quantity)
for all items related to the invoice.
"""
return self.annotate(
total=Subquery(
InvoiceItem.objects.filter(invoice_id=OuterRef("pk"))
.annotate(total=Sum(F("product_unit_price") * F("quantity")))
.values("total")
)
)
class Invoice(models.Model): class Invoice(models.Model):
"""Invoices are generated once the payment has been validated.""" """Invoices are generated once the payment has been validated."""
@ -173,6 +189,8 @@ class Invoice(models.Model):
date = models.DateTimeField(_("date"), auto_now=True) date = models.DateTimeField(_("date"), auto_now=True)
validated = models.BooleanField(_("validated"), default=False) validated = models.BooleanField(_("validated"), default=False)
objects = InvoiceQueryset.as_manager()
def __str__(self): def __str__(self):
return f"{self.user} - {self.get_total()} - {self.date}" return f"{self.user} - {self.get_total()} - {self.date}"