Refactor Account Operations stats
K-Fêt - Statistics New base class - StatScale - create scale, given chunk size (timedelta), start and end times - get labels of - get start and end datetimes of chunks DayStatScale: Scale whose chunks interval is 1 day WeekStatScale: same with 1 week MonthStatScale: same with 1 month AccountStatOperationList: manifest of operations stats of an account - renamed from AccountStatLastAll - updated according to SingleResumeStat AccountStatOperation: - renamed from AccountStatLast - remove scale logic with use of StatScale objects - used scale is given by `scale` and `scale_args` GET params - add filter on operations types with `types` GET param AccountStatLast(Day,Week,Month) are deleted ("merged" in AccountStatOperation)
This commit is contained in:
parent
31261fd376
commit
f585247224
|
@ -1,70 +1,117 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
|
||||||
KFET_WAKES_UP_AT = 7
|
KFET_WAKES_UP_AT = 7
|
||||||
|
|
||||||
# donne le nom des jours d'une liste de dates
|
|
||||||
# dans un dico ordonné
|
def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
|
||||||
def daynames(dates):
|
return timezone.datetime(year, month, day, hour=start_at)
|
||||||
names = {}
|
|
||||||
for i in dates:
|
|
||||||
names[i] = dates[i].strftime("%A")
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
# donne le nom des semaines une liste de dates
|
def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
|
||||||
# dans un dico ordonné
|
kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day)
|
||||||
def weeknames(dates):
|
if dt.hour < start_at:
|
||||||
names = {}
|
kfet_dt -= timezone.timedelta(days=1)
|
||||||
for i in dates:
|
return kfet_dt
|
||||||
names[i] = dates[i].strftime("Semaine %W")
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
# donne le nom des mois d'une liste de dates
|
class StatScale(object):
|
||||||
# dans un dico ordonné
|
name = None
|
||||||
def monthnames(dates):
|
step = None
|
||||||
names = {}
|
|
||||||
for i in dates:
|
def __init__(self, n_steps=0, begin=None, end=None,
|
||||||
names[i] = dates[i].strftime("%B")
|
last=False, std_chunk=True):
|
||||||
return names
|
self.std_chunk = std_chunk
|
||||||
|
if last:
|
||||||
|
end = timezone.now()
|
||||||
|
|
||||||
|
if begin is not None and n_steps != 0:
|
||||||
|
self.begin = self.get_from(begin)
|
||||||
|
self.end = self.do_step(self.begin, n_steps=n_steps)
|
||||||
|
elif end is not None and n_steps != 0:
|
||||||
|
self.end = self.get_from(end)
|
||||||
|
self.begin = self.do_step(self.end, n_steps=-n_steps)
|
||||||
|
elif begin is not None and end is not None:
|
||||||
|
self.begin = self.get_from(begin)
|
||||||
|
self.end = self.get_from(end)
|
||||||
|
else:
|
||||||
|
raise Exception('Two of these args must be specified: '
|
||||||
|
'n_steps, begin, end; '
|
||||||
|
'or use last and n_steps')
|
||||||
|
|
||||||
|
self.datetimes = self.get_datetimes()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def by_name(name):
|
||||||
|
for cls in StatScale.__subclasses__():
|
||||||
|
if cls.name == name:
|
||||||
|
return cls
|
||||||
|
raise Exception('scale %s not found' % name)
|
||||||
|
|
||||||
|
def get_from(self, dt):
|
||||||
|
return self.std_chunk and self.get_chunk_start(dt) or dt
|
||||||
|
|
||||||
|
def __getitem__(self, i):
|
||||||
|
return self.datetimes[i], self.datetimes[i+1]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.datetimes) - 1
|
||||||
|
|
||||||
|
def do_step(self, dt, n_steps=1):
|
||||||
|
return dt + self.step * n_steps
|
||||||
|
|
||||||
|
def get_datetimes(self):
|
||||||
|
datetimes = [self.begin]
|
||||||
|
tmp = self.begin
|
||||||
|
while tmp <= self.end:
|
||||||
|
tmp = self.do_step(tmp)
|
||||||
|
datetimes.append(tmp)
|
||||||
|
return datetimes
|
||||||
|
|
||||||
|
def get_labels(self, label_fmt=None):
|
||||||
|
if label_fmt is None:
|
||||||
|
label_fmt = self.label_fmt
|
||||||
|
return [begin.strftime(label_fmt) for begin, end in self]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_chunk_start(cls, dt):
|
||||||
|
dt_kfet = to_kfet_day(dt)
|
||||||
|
start = dt_kfet - cls.offset_to_chunk_start(dt_kfet)
|
||||||
|
return start
|
||||||
|
|
||||||
|
|
||||||
# rend les dates des nb derniers jours
|
class DayStatScale(StatScale):
|
||||||
# dans l'ordre chronologique
|
name = 'day'
|
||||||
# aujourd'hui compris
|
step = timezone.timedelta(days=1)
|
||||||
# nb = 1 : rend hier
|
label_fmt = '%A'
|
||||||
def lastdays(nb):
|
|
||||||
morning = this_morning()
|
@classmethod
|
||||||
days = {}
|
def get_chunk_start(cls, dt):
|
||||||
for i in range(1, nb+1):
|
return to_kfet_day(dt)
|
||||||
days[i] = morning - timezone.timedelta(days=nb - i + 1)
|
|
||||||
return days
|
|
||||||
|
|
||||||
|
|
||||||
def lastweeks(nb):
|
class WeekStatScale(StatScale):
|
||||||
monday_morning = this_monday_morning()
|
name = 'week'
|
||||||
mondays = {}
|
step = timezone.timedelta(days=7)
|
||||||
for i in range(1, nb+1):
|
label_fmt = 'Semaine %W'
|
||||||
mondays[i] = monday_morning \
|
|
||||||
- timezone.timedelta(days=7*(nb - i + 1))
|
@classmethod
|
||||||
return mondays
|
def offset_to_chunk_start(cls, dt):
|
||||||
|
return timezone.timedelta(days=dt.weekday())
|
||||||
|
|
||||||
|
|
||||||
def lastmonths(nb):
|
class MonthStatScale(StatScale):
|
||||||
first_month_day = this_first_month_day()
|
name = 'month'
|
||||||
first_days = {}
|
step = relativedelta(months=1)
|
||||||
this_year = first_month_day.year
|
label_fmt = '%B'
|
||||||
this_month = first_month_day.month
|
|
||||||
for i in range(1, nb+1):
|
@classmethod
|
||||||
month = ((this_month - 1 - (nb - i)) % 12) + 1
|
def get_chunk_start(cls, dt):
|
||||||
year = this_year + (nb - i) // 12
|
return to_kfet_day(dt).replace(day=1)
|
||||||
first_days[i] = timezone.datetime(year=year,
|
|
||||||
month=month,
|
|
||||||
day=1,
|
|
||||||
hour=KFET_WAKES_UP_AT)
|
|
||||||
return first_days
|
|
||||||
|
|
||||||
|
|
||||||
def this_first_month_day():
|
def this_first_month_day():
|
||||||
|
|
18
kfet/urls.py
18
kfet/urls.py
|
@ -69,18 +69,12 @@ urlpatterns = [
|
||||||
name='kfet.account.negative'),
|
name='kfet.account.negative'),
|
||||||
|
|
||||||
# Account - Statistics
|
# Account - Statistics
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/$',
|
url('^accounts/(?P<trigramme>.{3})/stat/operations/list$',
|
||||||
views.AccountStatLastAll.as_view(),
|
views.AccountStatOperationList.as_view(),
|
||||||
name = 'kfet.account.stat.last'),
|
name='kfet.account.stat.operation.list'),
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/month/$',
|
url('^accounts/(?P<trigramme>.{3})/stat/operations$',
|
||||||
views.AccountStatLastMonth.as_view(),
|
views.AccountStatOperation.as_view(),
|
||||||
name = 'kfet.account.stat.last.month'),
|
name='kfet.account.stat.operation'),
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/week/$',
|
|
||||||
views.AccountStatLastWeek.as_view(),
|
|
||||||
name = 'kfet.account.stat.last.week'),
|
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/day/$',
|
|
||||||
views.AccountStatLastDay.as_view(),
|
|
||||||
name = 'kfet.account.stat.last.day'),
|
|
||||||
|
|
||||||
url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$',
|
url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$',
|
||||||
views.AccountStatBalanceList.as_view(),
|
views.AccountStatBalanceList.as_view(),
|
||||||
|
|
185
kfet/views.py
185
kfet/views.py
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import ast
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
@ -47,10 +48,10 @@ from decimal import Decimal
|
||||||
import django_cas_ng
|
import django_cas_ng
|
||||||
import heapq
|
import heapq
|
||||||
import statistics
|
import statistics
|
||||||
from .statistic import daynames, monthnames, weeknames, \
|
from kfet.statistic import DayStatScale, MonthStatScale, StatScale, WeekStatScale
|
||||||
lastdays, lastweeks, lastmonths, \
|
from .statistic import (
|
||||||
this_morning, this_monday_morning, this_first_month_day, \
|
this_morning, this_monday_morning, this_first_month_day, tot_ventes,
|
||||||
tot_ventes
|
)
|
||||||
|
|
||||||
class Home(TemplateView):
|
class Home(TemplateView):
|
||||||
template_name = "kfet/home.html"
|
template_name = "kfet/home.html"
|
||||||
|
@ -2282,138 +2283,128 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
|
||||||
|
|
||||||
# Un résumé de toutes les vues ArticleStatLast
|
# Un résumé de toutes les vues ArticleStatLast
|
||||||
# NE REND PAS DE JSON
|
# NE REND PAS DE JSON
|
||||||
class AccountStatLastAll(SingleResumeStat):
|
class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
|
||||||
model = Account
|
model = Account
|
||||||
context_object_name = 'account'
|
context_object_name = 'account'
|
||||||
trigramme_url_kwarg = 'trigramme'
|
pk_url_kwarg = 'trigramme'
|
||||||
id_prefix = ID_PREFIX_ACC_LAST
|
id_prefix = ID_PREFIX_ACC_LAST
|
||||||
nb_stat = 3
|
|
||||||
nb_default = 2
|
nb_default = 2
|
||||||
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
|
stats = [
|
||||||
stat_urls = ['kfet.account.stat.last.month',
|
{
|
||||||
'kfet.account.stat.last.week',
|
'label': 'Derniers mois',
|
||||||
'kfet.account.stat.last.day']
|
'url_params': dict(
|
||||||
|
types=[Operation.PURCHASE],
|
||||||
|
scale=MonthStatScale.name,
|
||||||
|
scale_args=dict(
|
||||||
|
last=True,
|
||||||
|
n_steps=7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Dernières semaines',
|
||||||
|
'url_params': dict(
|
||||||
|
types=[Operation.PURCHASE],
|
||||||
|
last_days=49,
|
||||||
|
scale=WeekStatScale.name,
|
||||||
|
scale_args=dict(
|
||||||
|
last=True,
|
||||||
|
n_steps=7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Derniers jours',
|
||||||
|
'url_params': dict(
|
||||||
|
types=[Operation.PURCHASE],
|
||||||
|
last_days=7,
|
||||||
|
scale=DayStatScale.name,
|
||||||
|
scale_args=dict(
|
||||||
|
last=True,
|
||||||
|
n_steps=7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
url_stat = 'kfet.account.stat.operation'
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
|
obj = super().get_object(*args, **kwargs)
|
||||||
return get_object_or_404(Account, trigramme=trigramme)
|
if self.request.user != obj.user:
|
||||||
|
raise PermissionDenied
|
||||||
def get_object_url_kwargs(self, **kwargs):
|
return obj
|
||||||
return {'trigramme': self.object.trigramme}
|
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AccountStatLast(JSONDetailView):
|
class AccountStatOperation(PkUrlMixin, JSONDetailView):
|
||||||
"""
|
"""
|
||||||
Returns a JSON containing the evolution a the personnal
|
Returns a JSON containing the evolution a the personnal
|
||||||
consommation of a trigramme at the diffent dates specified
|
consommation of a trigramme at the diffent dates specified
|
||||||
"""
|
"""
|
||||||
model = Account
|
model = Account
|
||||||
trigramme_url_kwarg = 'trigramme'
|
pk_url_kwarg = 'trigramme'
|
||||||
context_object_name = 'account'
|
context_object_name = 'account'
|
||||||
end_date = timezone.now()
|
|
||||||
id_prefix = ""
|
id_prefix = ""
|
||||||
|
|
||||||
# doit rendre un dictionnaire des dates
|
def get_operations(self, types, scale):
|
||||||
# la première date correspond au début
|
|
||||||
# la dernière date est la fin de la dernière plage
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# doit rendre un dictionnaire des labels
|
|
||||||
# le dernier label ne sera pas utilisé
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
|
||||||
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
|
|
||||||
return get_object_or_404(Account, trigramme=trigramme)
|
|
||||||
|
|
||||||
def sort_operations(self, **kwargs):
|
|
||||||
# On récupère les dates
|
|
||||||
dates = self.get_dates()
|
|
||||||
# On ajoute la date de fin
|
|
||||||
extended_dates = dates.copy()
|
|
||||||
extended_dates[len(dates)+1] = self.end_date
|
|
||||||
# On selectionne les opérations qui correspondent
|
# On selectionne les opérations qui correspondent
|
||||||
# à l'article en question et qui ne sont pas annulées
|
# à l'article en question et qui ne sont pas annulées
|
||||||
# puis on choisi pour chaques intervalle les opérations
|
# puis on choisi pour chaques intervalle les opérations
|
||||||
# effectuées dans ces intervalles de temps
|
# effectuées dans ces intervalles de temps
|
||||||
all_operations = (Operation.objects
|
all_operations = (Operation.objects
|
||||||
.filter(type='purchase')
|
.filter(type__in=types)
|
||||||
.filter(group__on_acc=self.object)
|
.filter(group__on_acc=self.object)
|
||||||
.filter(canceled_at=None)
|
.filter(canceled_at=None)
|
||||||
)
|
)
|
||||||
operations = {}
|
operations = []
|
||||||
for i in dates:
|
for begin, end in scale:
|
||||||
operations[i] = (all_operations
|
operations.append(all_operations
|
||||||
.filter(group__at__gte=extended_dates[i])
|
.filter(group__at__gte=begin,
|
||||||
.filter(group__at__lte=extended_dates[i+1])
|
group__at__lte=end))
|
||||||
)
|
|
||||||
return operations
|
return operations
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = {}
|
context = {}
|
||||||
nb_ventes = {}
|
|
||||||
|
scale = self.request.GET.get('scale', None)
|
||||||
|
if scale is None:
|
||||||
|
scale = DayStatScale(n_steps=7, last=True)
|
||||||
|
else:
|
||||||
|
scale_cls = StatScale.by_name(scale)
|
||||||
|
scale_args = self.request.GET.get('scale_args', '{}')
|
||||||
|
scale = scale_cls(**ast.literal_eval(scale_args))
|
||||||
|
print(scale.datetimes)
|
||||||
|
|
||||||
|
types = self.request.GET.get('types', None)
|
||||||
|
if types is not None:
|
||||||
|
types = ast.literal_eval(types)
|
||||||
|
|
||||||
|
operations = self.get_operations(types=types, scale=scale)
|
||||||
# On récupère les labels des dates
|
# On récupère les labels des dates
|
||||||
context['labels'] = self.get_labels().copy()
|
context['labels'] = scale.get_labels()
|
||||||
# On compte les opérations
|
# On compte les opérations
|
||||||
operations = self.sort_operations()
|
nb_ventes = []
|
||||||
for i in operations:
|
for chunk in operations:
|
||||||
nb_ventes[i] = tot_ventes(operations[i])
|
nb_ventes.append(tot_ventes(chunk))
|
||||||
|
|
||||||
|
context['labels'] = scale.get_labels()
|
||||||
context['charts'] = [{"color": "rgb(255, 99, 132)",
|
context['charts'] = [{"color": "rgb(255, 99, 132)",
|
||||||
"label": "NB items achetés",
|
"label": "NB items achetés",
|
||||||
"values": nb_ventes}]
|
"values": nb_ventes}]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_object(self, *args, **kwargs):
|
||||||
|
obj = super().get_object(*args, **kwargs)
|
||||||
|
if self.request.user != obj.user:
|
||||||
|
raise PermissionDenied
|
||||||
|
return obj
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(AccountStatLast, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Rend les achats pour ce compte des 7 derniers jours
|
|
||||||
# Aujourd'hui non compris
|
|
||||||
class AccountStatLastDay(AccountStatLast):
|
|
||||||
end_date = this_morning()
|
|
||||||
id_prefix = ID_PREFIX_ACC_LAST_DAYS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastdays(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
days = lastdays(7)
|
|
||||||
return daynames(days)
|
|
||||||
|
|
||||||
|
|
||||||
# Rend les achats de ce compte des 7 dernières semaines
|
|
||||||
# La semaine en cours n'est pas comprise
|
|
||||||
class AccountStatLastWeek(AccountStatLast):
|
|
||||||
end_date = this_monday_morning()
|
|
||||||
id_prefix = ID_PREFIX_ACC_LAST_WEEKS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastweeks(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
weeks = lastweeks(7)
|
|
||||||
return weeknames(weeks)
|
|
||||||
|
|
||||||
|
|
||||||
# Rend les achats de ce compte des 7 derniers mois
|
|
||||||
# Le mois en cours n'est pas compris
|
|
||||||
class AccountStatLastMonth(AccountStatLast):
|
|
||||||
end_date = this_monday_morning()
|
|
||||||
id_prefix = ID_PREFIX_ACC_LAST_MONTHS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastmonths(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
months = lastmonths(7)
|
|
||||||
return monthnames(months)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
|
|
Loading…
Reference in a new issue