mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 06:03:20 +00:00
Merge pull request #859 from ae-utbm/account-pages
Optimize user account pages
This commit is contained in:
commit
2111a2c67e
@ -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>
|
||||||
|
@ -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><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
|
||||||
<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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from model_bakery import baker, seq
|
from model_bakery import baker, seq
|
||||||
@ -95,3 +96,18 @@ class TestSearchUsersView(TestSearchUsers):
|
|||||||
self.client.force_login(subscriber_user.make())
|
self.client.force_login(subscriber_user.make())
|
||||||
response = self.client.get(reverse("core:search"))
|
response = self.client.get(reverse("core:search"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_account_not_found(client: Client):
|
||||||
|
client.force_login(baker.make(User, is_superuser=True))
|
||||||
|
user = baker.make(User)
|
||||||
|
res = client.get(reverse("core:user_account", kwargs={"user_id": user.id}))
|
||||||
|
assert res.status_code == 404
|
||||||
|
res = client.get(
|
||||||
|
reverse(
|
||||||
|
"core:user_account_detail",
|
||||||
|
kwargs={"user_id": user.id, "year": 2024, "month": 10},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
@ -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,65 +618,57 @@ 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
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
obj = super().get_object(queryset)
|
||||||
|
if not hasattr(obj, "customer"):
|
||||||
|
raise Http404(_("User has no account"))
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class UserAccountView(UserAccountBase):
|
class UserAccountView(UserAccountBase):
|
||||||
"""Display a user's account."""
|
"""Display a user's account."""
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -685,13 +680,29 @@ class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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}"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user