Merge branch 'Aufinal/stat_2' into 'master'
Repassage sur les stats Closes #246 and #255 See merge request klub-dev-ens/gestioCOF!462
This commit is contained in:
commit
57901c0013
5 changed files with 137 additions and 131 deletions
|
@ -24,6 +24,7 @@ from kfet.models import (
|
|||
Transfer,
|
||||
TransferGroup,
|
||||
)
|
||||
from kfet.statistic import SCALE_CLASS_CHOICES
|
||||
|
||||
from . import KFET_DELETED_TRIGRAMME
|
||||
from .auth import KFET_GENERIC_TRIGRAMME
|
||||
|
@ -601,3 +602,28 @@ class OrderArticleToInventoryForm(forms.Form):
|
|||
self.category = kwargs["initial"]["category"]
|
||||
self.category_name = kwargs["initial"]["category__name"]
|
||||
self.quantity_ordered = kwargs["initial"]["quantity_ordered"]
|
||||
|
||||
|
||||
# ----
|
||||
# Formulaires pour les statistiques K-Fêt
|
||||
# ----
|
||||
|
||||
|
||||
class StatScaleForm(forms.Form):
|
||||
"""Formulaire pour nettoyer les paramètres envoyés aux
|
||||
vues de statistiques K-Fêt. Non destiné à être affiché.
|
||||
"""
|
||||
|
||||
name = forms.ChoiceField(choices=SCALE_CLASS_CHOICES)
|
||||
begin = forms.DateTimeField(required=False)
|
||||
end = forms.DateTimeField(required=False)
|
||||
n_steps = forms.IntegerField(required=False)
|
||||
last = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
class AccountStatForm(forms.Form):
|
||||
""" Idem, mais pour la balance d'un compte """
|
||||
|
||||
begin_date = forms.DateTimeField(required=False)
|
||||
end_date = forms.DateTimeField(required=False)
|
||||
last_days = forms.IntegerField(required=False)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
from dateutil.parser import parse as dateutil_parse
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.utils import timezone
|
||||
|
||||
|
@ -48,10 +47,10 @@ class Scale(object):
|
|||
if end is not None:
|
||||
end = self.do_step(self.get_chunk_start(end))
|
||||
|
||||
if begin is not None and n_steps != 0:
|
||||
if begin is not None and n_steps:
|
||||
self.begin = begin
|
||||
self.end = self.do_step(self.begin, n_steps=n_steps)
|
||||
elif end is not None and n_steps != 0:
|
||||
elif end is not None and n_steps:
|
||||
self.end = end
|
||||
self.begin = self.do_step(self.end, n_steps=-n_steps)
|
||||
elif begin is not None and end is not None:
|
||||
|
@ -144,67 +143,24 @@ class MonthScale(Scale):
|
|||
return to_kfet_day(dt).replace(day=1)
|
||||
|
||||
|
||||
def scale_url_params(scales_def, **other_url_params):
|
||||
SCALE_CLASS_CHOICES = ((cls.name, cls.name) for cls in Scale.__subclasses__())
|
||||
SCALE_DICT = {cls.name: cls for cls in Scale.__subclasses__()}
|
||||
|
||||
|
||||
def scale_url_params(scales_def):
|
||||
"""
|
||||
Convertit une spécification de scales en arguments GET utilisables par ScaleMixin.
|
||||
La spécification est de la forme suivante :
|
||||
- scales_def : liste de champs de la forme (label, scale)
|
||||
- scales_def : liste de champs de la forme (label, scale, scale_args, default)
|
||||
|
||||
- scale_args : arguments à passer à Scale.__init__
|
||||
- other_url_params : paramètres GET supplémentaires
|
||||
- default : le graphe à montrer par défaut
|
||||
"""
|
||||
|
||||
params_list = []
|
||||
for label, cls, params, default in scales_def:
|
||||
url_params = {"scale_name": cls.name}
|
||||
url_params.update({"scale_" + key: value for key, value in params.items()})
|
||||
url_params.update(other_url_params)
|
||||
url_params = {"scale-name": cls.name}
|
||||
url_params.update({"scale-" + key: value for key, value in params.items()})
|
||||
params_list.append(dict(label=label, url_params=url_params, default=default))
|
||||
|
||||
return params_list
|
||||
|
||||
|
||||
class ScaleMixin(object):
|
||||
def parse_scale_args(self):
|
||||
"""
|
||||
Récupère les paramètres de subdivision encodés dans une requête GET.
|
||||
"""
|
||||
scale_args = {}
|
||||
|
||||
name = self.request.GET.get("scale_name", None)
|
||||
if name is not None:
|
||||
scale_args["name"] = name
|
||||
|
||||
n_steps = self.request.GET.get("scale_n_steps", None)
|
||||
if n_steps is not None:
|
||||
scale_args["n_steps"] = int(n_steps)
|
||||
|
||||
begin = self.request.GET.get("scale_begin", None)
|
||||
if begin is not None:
|
||||
scale_args["begin"] = dateutil_parse(begin)
|
||||
|
||||
end = self.request.GET.get("scale_send", None)
|
||||
if end is not None:
|
||||
scale_args["end"] = dateutil_parse(end)
|
||||
|
||||
last = self.request.GET.get("scale_last", None)
|
||||
if last is not None:
|
||||
scale_args["last"] = last in ["true", "True", "1"] and True or False
|
||||
|
||||
return scale_args
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
# On n'hérite pas
|
||||
|
||||
scale_args = self.parse_scale_args()
|
||||
scale_name = scale_args.pop("name", None)
|
||||
scale_cls = Scale.by_name(scale_name)
|
||||
|
||||
if scale_cls is None:
|
||||
self.scale = self.get_default_scale()
|
||||
else:
|
||||
self.scale = scale_cls(**scale_args)
|
||||
|
||||
return {"labels": self.scale.get_labels()}
|
||||
|
||||
def get_default_scale(self):
|
||||
return DayScale(n_steps=7, last=True)
|
||||
|
|
|
@ -44,9 +44,9 @@ class TestStats(TestCase):
|
|||
"/k-fet/accounts/FOO/stat/operations?{}".format(
|
||||
"&".join(
|
||||
[
|
||||
"scale=day",
|
||||
"types=['purchase']",
|
||||
"scale_args={'n_steps':+7,+'last':+True}",
|
||||
"scale-name=day",
|
||||
"scale-n_steps=7",
|
||||
"scale-last=True",
|
||||
"format=json",
|
||||
]
|
||||
)
|
||||
|
@ -64,7 +64,17 @@ class TestStats(TestCase):
|
|||
# receives a Redirect response
|
||||
articles_urls = [
|
||||
"/k-fet/articles/{}/stat/sales/list".format(article.pk),
|
||||
"/k-fet/articles/{}/stat/sales".format(article.pk),
|
||||
"/k-fet/articles/{}/stat/sales?{}".format(
|
||||
article.pk,
|
||||
"&".join(
|
||||
[
|
||||
"scale-name=day",
|
||||
"scale-n_steps=7",
|
||||
"scale-last=True",
|
||||
"format=json",
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
for url in articles_urls:
|
||||
resp = client.get(url)
|
||||
|
|
|
@ -651,10 +651,9 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"types": ["['purchase']"],
|
||||
"scale_name": ["month"],
|
||||
"scale_last": ["True"],
|
||||
"scale_begin": [
|
||||
"scale-name": ["month"],
|
||||
"scale-last": ["True"],
|
||||
"scale-begin": [
|
||||
self.accounts["user1"].created_at.isoformat(" ")
|
||||
],
|
||||
},
|
||||
|
@ -665,10 +664,9 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"types": ["['purchase']"],
|
||||
"scale_n_steps": ["12"],
|
||||
"scale_name": ["month"],
|
||||
"scale_last": ["True"],
|
||||
"scale-n_steps": ["12"],
|
||||
"scale-name": ["month"],
|
||||
"scale-last": ["True"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -677,10 +675,9 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"types": ["['purchase']"],
|
||||
"scale_n_steps": ["13"],
|
||||
"scale_name": ["week"],
|
||||
"scale_last": ["True"],
|
||||
"scale-n_steps": ["13"],
|
||||
"scale-name": ["week"],
|
||||
"scale-last": ["True"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -689,10 +686,9 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"types": ["['purchase']"],
|
||||
"scale_n_steps": ["14"],
|
||||
"scale_name": ["day"],
|
||||
"scale_last": ["True"],
|
||||
"scale-n_steps": ["14"],
|
||||
"scale-name": ["day"],
|
||||
"scale-last": ["True"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -739,7 +735,9 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase):
|
|||
return {"user1": create_user("user1", "001")}
|
||||
|
||||
def test_ok(self):
|
||||
r = self.client.get(self.url)
|
||||
r = self.client.get(
|
||||
self.url, {"scale-name": "day", "scale-n_steps": 7, "scale-last": True}
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
|
@ -1593,9 +1591,9 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"scale_name": ["month"],
|
||||
"scale_last": ["True"],
|
||||
"scale_begin": [self.opegroup.at.isoformat(" ")],
|
||||
"scale-name": ["month"],
|
||||
"scale-last": ["True"],
|
||||
"scale-begin": [self.opegroup.at.strftime("%Y-%m-%d %H:%M:%S")],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1604,9 +1602,9 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"scale_n_steps": ["12"],
|
||||
"scale_name": ["month"],
|
||||
"scale_last": ["True"],
|
||||
"scale-n_steps": ["12"],
|
||||
"scale-name": ["month"],
|
||||
"scale-last": ["True"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1615,9 +1613,9 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"scale_n_steps": ["13"],
|
||||
"scale_name": ["week"],
|
||||
"scale_last": ["True"],
|
||||
"scale-n_steps": ["13"],
|
||||
"scale-name": ["week"],
|
||||
"scale-last": ["True"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1626,9 +1624,9 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
|
|||
"url": {
|
||||
"path": base_url,
|
||||
"query": {
|
||||
"scale_n_steps": ["14"],
|
||||
"scale_name": ["day"],
|
||||
"scale_last": ["True"],
|
||||
"scale-n_steps": ["14"],
|
||||
"scale-name": ["day"],
|
||||
"scale-last": ["True"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1661,7 +1659,9 @@ class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase):
|
|||
)
|
||||
|
||||
def test_ok(self):
|
||||
r = self.client.get(self.url)
|
||||
r = self.client.get(
|
||||
self.url, {"scale-name": "day", "scale-n_steps": 7, "scale-last": True}
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ast
|
||||
import heapq
|
||||
import statistics
|
||||
from collections import defaultdict
|
||||
|
@ -12,6 +11,7 @@ from django.contrib.auth.decorators import login_required, permission_required
|
|||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, Prefetch, Q, Sum
|
||||
from django.forms import formset_factory
|
||||
|
@ -36,6 +36,7 @@ from kfet.forms import (
|
|||
AccountNoTriForm,
|
||||
AccountPwdForm,
|
||||
AccountRestrictForm,
|
||||
AccountStatForm,
|
||||
AccountTriForm,
|
||||
AddcostForm,
|
||||
ArticleForm,
|
||||
|
@ -55,6 +56,7 @@ from kfet.forms import (
|
|||
KPsulOperationGroupForm,
|
||||
OrderArticleForm,
|
||||
OrderArticleToInventoryForm,
|
||||
StatScaleForm,
|
||||
TransferFormSet,
|
||||
UserForm,
|
||||
UserGroupForm,
|
||||
|
@ -78,7 +80,7 @@ from kfet.models import (
|
|||
Transfer,
|
||||
TransferGroup,
|
||||
)
|
||||
from kfet.statistic import DayScale, MonthScale, ScaleMixin, WeekScale, scale_url_params
|
||||
from kfet.statistic import SCALE_DICT, DayScale, MonthScale, WeekScale, scale_url_params
|
||||
from shared.views import AutocompleteView
|
||||
|
||||
from .auth import KFET_GENERIC_TRIGRAMME
|
||||
|
@ -2304,6 +2306,26 @@ class UserAccountMixin:
|
|||
return obj
|
||||
|
||||
|
||||
class ScaleMixin(object):
|
||||
"""Mixin pour utiliser les outils de `kfet.statistic`."""
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
# On n'hérite pas
|
||||
form = StatScaleForm(self.request.GET, prefix="scale")
|
||||
|
||||
if not form.is_valid():
|
||||
raise SuspiciousOperation(
|
||||
"Invalid StatScaleForm. Did someone tamper with the GET parameters ?"
|
||||
)
|
||||
|
||||
scale_name = form.cleaned_data.pop("name")
|
||||
scale_cls = SCALE_DICT.get(scale_name)
|
||||
|
||||
self.scale = scale_cls(**form.cleaned_data)
|
||||
|
||||
return {"labels": self.scale.get_labels()}
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Evolution Balance perso
|
||||
# -----------------------
|
||||
|
@ -2416,16 +2438,15 @@ class AccountStatBalance(UserAccountMixin, JSONDetailView):
|
|||
def get_context_data(self, *args, **kwargs):
|
||||
context = {}
|
||||
|
||||
last_days = self.request.GET.get("last_days", None)
|
||||
if last_days is not None:
|
||||
last_days = int(last_days)
|
||||
begin_date = self.request.GET.get("begin_date", None)
|
||||
end_date = self.request.GET.get("end_date", None)
|
||||
form = AccountStatForm(self.request.GET)
|
||||
|
||||
changes = self.get_changes_list(
|
||||
last_days=last_days, begin_date=begin_date, end_date=end_date
|
||||
if not form.is_valid():
|
||||
raise SuspiciousOperation(
|
||||
"Invalid AccountStatForm. Did someone tamper with the GET parameters ?"
|
||||
)
|
||||
|
||||
changes = self.get_changes_list(**form.cleaned_data)
|
||||
|
||||
context["charts"] = [
|
||||
{"color": "rgb(200, 20, 60)", "label": "Balance", "values": changes}
|
||||
]
|
||||
|
@ -2466,7 +2487,7 @@ class AccountStatOperationList(UserAccountMixin, SingleResumeStat):
|
|||
("2 semaines", DayScale, {"last": True, "n_steps": 14}, False),
|
||||
]
|
||||
|
||||
return scale_url_params(scales_def, types=[Operation.PURCHASE])
|
||||
return scale_url_params(scales_def)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -2479,28 +2500,16 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView):
|
|||
slug_url_kwarg = "trigramme"
|
||||
slug_field = "trigramme"
|
||||
|
||||
def get_operations(self, types=None):
|
||||
# On selectionne les opérations qui correspondent
|
||||
# à l'article en question et qui ne sont pas annulées
|
||||
# puis on choisi pour chaques intervalle les opérations
|
||||
# effectuées dans ces intervalles de temps
|
||||
all_operations = (
|
||||
Operation.objects.filter(group__on_acc=self.object, canceled_at=None)
|
||||
.values("article_nb", "group__at")
|
||||
.order_by("group__at")
|
||||
)
|
||||
if types is not None:
|
||||
all_operations = all_operations.filter(type__in=types)
|
||||
return all_operations
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
|
||||
types = self.request.GET.get("types", None)
|
||||
if types is not None:
|
||||
types = ast.literal_eval(types)
|
||||
|
||||
operations = self.get_operations(types=types)
|
||||
operations = (
|
||||
Operation.objects.filter(
|
||||
type=Operation.PURCHASE, group__on_acc=self.object, canceled_at=None
|
||||
)
|
||||
.values("article_nb", "group__at")
|
||||
.order_by("group__at")
|
||||
)
|
||||
# On compte les opérations
|
||||
nb_ventes = self.scale.chunkify_qs(
|
||||
operations, field="group__at", aggregate=Sum("article_nb")
|
||||
|
@ -2517,7 +2526,7 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView):
|
|||
|
||||
|
||||
# ------------------------
|
||||
# Article Satistiques Last
|
||||
# Article Statistiques Last
|
||||
# ------------------------
|
||||
|
||||
|
||||
|
@ -2542,7 +2551,12 @@ class ArticleStatSalesList(SingleResumeStat):
|
|||
# On le crée dans le passé au cas où
|
||||
first_conso = timezone.now() - timedelta(seconds=1)
|
||||
scales_def = [
|
||||
("Tout le temps", MonthScale, {"last": True, "begin": first_conso}, False),
|
||||
(
|
||||
"Tout le temps",
|
||||
MonthScale,
|
||||
{"last": True, "begin": first_conso.strftime("%Y-%m-%d %H:%M:%S")},
|
||||
False,
|
||||
),
|
||||
("1 an", MonthScale, {"last": True, "n_steps": 12}, False),
|
||||
("3 mois", WeekScale, {"last": True, "n_steps": 13}, True),
|
||||
("2 semaines", DayScale, {"last": True, "n_steps": 14}, False),
|
||||
|
@ -2572,16 +2586,16 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
|
|||
.values("group__at", "article_nb")
|
||||
.order_by("group__at")
|
||||
)
|
||||
liq_only = all_purchases.filter(group__on_acc__trigramme="LIQ")
|
||||
liq_exclude = all_purchases.exclude(group__on_acc__trigramme="LIQ")
|
||||
cof_accts = all_purchases.filter(group__on_acc__cofprofile__is_cof=True)
|
||||
noncof_accts = all_purchases.exclude(group__on_acc__cofprofile__is_cof=True)
|
||||
|
||||
nb_liq = scale.chunkify_qs(
|
||||
liq_only, field="group__at", aggregate=Sum("article_nb")
|
||||
nb_cof = scale.chunkify_qs(
|
||||
cof_accts, field="group__at", aggregate=Sum("article_nb")
|
||||
)
|
||||
nb_accounts = scale.chunkify_qs(
|
||||
liq_exclude, field="group__at", aggregate=Sum("article_nb")
|
||||
nb_noncof = scale.chunkify_qs(
|
||||
noncof_accts, field="group__at", aggregate=Sum("article_nb")
|
||||
)
|
||||
nb_ventes = [n1 + n2 for n1, n2 in zip(nb_liq, nb_accounts)]
|
||||
nb_ventes = [n1 + n2 for n1, n2 in zip(nb_cof, nb_noncof)]
|
||||
|
||||
context["charts"] = [
|
||||
{
|
||||
|
@ -2589,11 +2603,11 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
|
|||
"label": "Toutes consommations",
|
||||
"values": nb_ventes,
|
||||
},
|
||||
{"color": "rgb(54, 162, 235)", "label": "LIQ", "values": nb_liq},
|
||||
{"color": "rgb(54, 162, 235)", "label": "LIQ", "values": nb_cof},
|
||||
{
|
||||
"color": "rgb(255, 205, 86)",
|
||||
"label": "Comptes K-Fêt",
|
||||
"values": nb_accounts,
|
||||
"values": nb_noncof,
|
||||
},
|
||||
]
|
||||
return context
|
||||
|
|
Loading…
Reference in a new issue