kpsul/kfet/statistic.py
2017-01-14 16:23:45 +01:00

491 lines
17 KiB
Python

# -*- coding: utf-8 -*-
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.generic import DetailView
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
from django.contrib.auth.decorators import login_required
from kfet.models import (Account, Article, Operation, OperationGroup, Transfer)
# Pareil mais pour une liste de dates
# dans un dico ordonné
def daynames(dates):
names = {}
for i in dates:
names[i] = dates[i].strftime("%A")
return names
# Pareil mais pour une liste de dates
# dans un dico ordonné
def weeknames(dates):
names = {}
for i in dates:
names[i] = "Semaine " + dates[i].strftime("%W")
return names
# Pareil mais pour une liste de dates
# dans un dico ordonné
def monthnames(dates):
names = {}
for i in dates:
names[i] = dates[i].strftime("%B")
return names
# rend les dates des nb derniers jours
# dans l'ordre chronologique
# aujourd'hui compris
# nb = 1 : rend hier
def lastdays(nb):
morning = this_morning()
days = {}
for i in range(1, nb+1):
days[i] = morning - timezone.timedelta(days=nb - i + 1)
return days
def lastweeks(nb):
monday_morning = this_monday_morning()
mondays = {}
for i in range(1, nb+1):
mondays[i] = monday_morning \
- timezone.timedelta(days=7*(nb - i + 1))
return mondays
def lastmonths(nb):
first_month_day = this_first_month_day()
first_days = {}
this_year = first_month_day.year
this_month = first_month_day.month
for i in range(1, nb+1):
month = this_month - (nb - i) % 12
year = this_year + (nb - i) // 12
first_days[i] = timezone.datetime(year=year,
month=month,
day=1)
return first_days
def this_first_month_day():
now = timezone.now()
first_day = timezone.datetime(year=now.year,
month=now.month,
day=1)
return first_day
def this_monday_morning():
now = timezone.now()
monday = now - timezone.timedelta(days=now.isoweekday()-1)
monday_morning = timezone.datetime(year=monday.year,
month=monday.month,
day=monday.day)
return monday_morning
def this_morning():
now = timezone.now()
morning = timezone.datetime(year=now.year,
month=now.month,
day=now.day)
return morning
# Étant donné un queryset d'operations
# rend la somme des article_nb
def tot_ventes(queryset):
res = 0
for op in queryset:
res += op.article_nb
return res
# -----
# Views
# -----
# ---------------
# Vues génériques
# ---------------
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
class JSONResponseMixin(object):
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(
self.get_data(context),
**response_kwargs
)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
class HybridDetailView(JSONResponseMixin,
SingleObjectTemplateResponseMixin,
BaseDetailView):
"""
Returns a DetailView as a html page except if it asked a JSON
file by the GET method in witch case it returns a JSON response.
"""
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format') == 'json':
return self.render_to_json_response(context)
else:
return super(HybridDetailView, self).render_to_response(context)
class HybridListView(JSONResponseMixin,
MultipleObjectTemplateResponseMixin,
BaseListView):
"""
Returns a ListView as a html page except if it asked a JSON
file by the GET method in witch case it returns a JSON response.
"""
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format') == 'json':
return self.render_to_json_response(context)
else:
return super(HybridListView, self).render_to_response(context)
class ObjectResumeStat(DetailView):
"""
Summarize all the stats of an object
DOES NOT RETURN A JSON RESPONSE
"""
template_name = 'kfet/object_stat_resume.html'
context_object_name = 'lul'
id_prefix = 'id_a_definir'
# nombre de vues à résumer
nb_stat = 2
# Le combienième est celui par defaut ?
# (entre 0 et nb_stat-1)
nb_default = 0
stat_labels = ['stat_1', 'stat_2']
stat_urls = ['url_1', 'url_2']
# sert à renverser les urls
# utile de le surcharger quand l'url prend d'autres arguments que l'id
def get_object_url_kwargs(self, **kwargs):
return {'pk': self.object.id}
def get_context_data(self, **kwargs):
# On hérite pas
object_id = self.object.id
context = {}
stats = {}
for i in range(self.nb_stat):
stats[i] = {
'label': self.stat_labels[i],
'btn': "btn_%s_%d_%d" % (self.id_prefix,
object_id,
i),
'url': reverse_lazy(self.stat_urls[i],
kwargs=self.get_object_url_kwargs()),
}
prefix = "%s_%d" % (self.id_prefix, object_id)
context['id_prefix'] = prefix
context['content_id'] = "content_%s" % prefix
context['stats'] = stats
context['default_stat'] = self.nb_default
context['object_id'] = object_id
return context
# ------------------
# AccountStatBalance
# ------------------
class AccountStatBalance(HybridDetailView):
"""
Returns a graph (or a JSON Response) of the evolution a the personnal
balance of a trigramm between begin_date and end_date
takes into account the Operations and the Transfers
does not takes intto account the balance offset
"""
model = Account
trigramme_url_kwarg = 'trigramme'
template_name = 'kfet/account_stat_balance.html'
context_object_name = 'account'
begin_date = this_morning()
end_date = timezone.now() # ne gère pas encore autre chose que now
anytime = False # un cas particulier
id_prefix = "lol"
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def get_changes_list(self, **kwargs):
account = self.object
# On récupère les opérations
# TODO: retirer les opgroup dont tous les op sont annulées
opgroups = list(OperationGroup.objects
.filter(on_acc=account)
.filter(at__gte=self.begin_date)
.filter(at__lte=self.end_date))
# On récupère les transferts reçus
received_transfers = list(Transfer.objects
.filter(to_acc=account)
.filter(canceled_at=None)
.filter(group__at__gte=self.begin_date)
.filter(group__at__lte=self.end_date))
# On récupère les transferts émis
emitted_transfers = list(Transfer.objects
.filter(from_acc=account)
.filter(canceled_at=None)
.filter(group__at__gte=self.begin_date)
.filter(group__at__lte=self.end_date))
# On transforme tout ça en une liste de dictionnaires sous la forme
# {'at': date,
# 'amount': changement de la balance (négatif si diminue la balance,
# positif si l'augmente),
# 'label': text descriptif,
# 'balance': état de la balance après l'action (0 pour le moment,
# sera mis à jour lors d'une
# autre passe)
# }
actions = [
# Maintenant (à changer si on gère autre chose que now)
{
'at': self.end_date.isoformat(),
'amout': 0,
'label': "actuel",
'balance': 0,
}
] + [
{
'at': op.at.isoformat(),
'amount': op.amount,
'label': str(op),
'balance': 0,
} for op in opgroups
] + [
{
'at': tr.group.at.isoformat(),
'amount': tr.amount,
'label': "%d€: %s -> %s" % (tr.amount,
tr.from_acc.trigramme,
tr.to_acc.trigramme),
'balance': 0,
} for tr in received_transfers
] + [
{
'at': tr.group.at.isoformat(),
'amount': -tr.amount,
'label': "%d€: %s -> %s" % (tr.amount,
tr.from_acc.trigramme,
tr.to_acc.trigramme),
'balance': 0,
} for tr in emitted_transfers
]
if not self.anytime:
actions += [
# Date de début :
{
'at': self.begin_date.isoformat(),
'amount': 0,
'label': "début",
'balance': 0,
}
]
# Maintenant on trie la liste des actions par ordre du plus récent
# an plus ancien et on met à jour la balance
actions = sorted(actions, key=lambda k: k['at'], reverse=True)
actions[0]['balance'] = account.balance
for i in range(len(actions)-1):
actions[i+1]['balance'] = actions[i]['balance'] \
- actions[i+1]['amount']
return actions
def get_context_data(self, **kwargs):
context = {}
changes = self.get_changes_list()
context['changes'] = changes
context['chart_id'] = "%s_%s" % (self.id_prefix,
self.object.id)
context['min_date'] = changes[len(changes)-1]['at']
context['max_date'] = changes[0]['at']
# TODO: offset
return context
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
# ---------------
# AccountStatLast
# ---------------
class AccountStatLast(HybridDetailView):
"""
Returns a graph (or a JSON Response) of the evolution a the personnal
consommation of a trigramm at the diffent dates precised
"""
model = Account
trigramme_url_kwarg = 'trigramme'
template_name = 'kfet/account_stat_last.html'
context_object_name = 'account'
end_date = timezone.now()
id_prefix = "lol"
# doit rendre un dictionnaire des dates
# 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):
pass
# 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
# à 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(type='purchase')
.filter(group__on_acc=self.object)
.filter(canceled_at=None)
)
operations = {}
for i in dates:
operations[i] = (all_operations
.filter(group__at__gte=extended_dates[i])
.filter(group__at__lte=extended_dates[i+1])
)
return operations
def get_context_data(self, **kwargs):
# On hérite
# en fait non, pas besoin et c'est chiant à dumper
# context = super(AccountStat, self).get_context_data(**kwargs)
context = {}
nb_ventes = {}
# On récupère les labels des dates
context['labels'] = self.get_labels().copy()
# On compte les opérations
operations = self.sort_operations()
for i in operations:
nb_ventes[i] = tot_ventes(operations[i])
context['nb_ventes'] = nb_ventes
# ID unique
context['chart_id'] = "%s_%d" % (self.id_prefix,
self.object.id)
return context
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatLast, self).dispatch(*args, **kwargs)
# ---------------
# ArticleStatLast
# ---------------
# Rend un graph des ventes sur une plage de temps à préciser.
# Le graphique distingue les ventes sur LIQ et sur les autres trigrammes
class ArticleStatLast(HybridDetailView):
"""
Returns a graph (or a JSON Response) of the consommation
of an article at the diffent dates precised
"""
model = Article
template_name = 'kfet/article_stat_last.html'
context_object_name = 'article'
end_date = timezone.now()
id_prefix = "lol"
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format') == 'json':
return self.render_to_json_response(context)
else:
return super(ArticleStatLast, self).render_to_response(context)
# doit rendre un dictionnaire des dates
# 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):
pass
# doit rendre un dictionnaire des labels
# le dernier label ne sera pas utilisé
def get_labels(self, **kwargs):
pass
def get_context_data(self, **kwargs):
# On hérite
# en fait non, pas besoin et c'est chiant à dumper
# context = super(ArticleStat, self).get_context_data(**kwargs)
context = {}
# On récupère les labels des dates
context['labels'] = self.get_labels().copy()
# 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
# à 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(type='purchase')
.filter(article=self.object)
.filter(canceled_at=None)
)
operations = {}
for i in dates:
operations[i] = (all_operations
.filter(group__at__gte=extended_dates[i])
.filter(group__at__lte=extended_dates[i+1])
)
# On compte les opérations
nb_ventes = {}
nb_accounts = {}
nb_liq = {}
for i in operations:
nb_ventes[i] = tot_ventes(operations[i])
nb_liq[i] = tot_ventes(
operations[i]
.filter(group__on_acc__trigramme='LIQ')
)
nb_accounts[i] = tot_ventes(
operations[i]
.exclude(group__on_acc__trigramme='LIQ')
)
context['nb_ventes'] = nb_ventes
context['nb_accounts'] = nb_accounts
context['nb_liq'] = nb_liq
# ID unique
context['chart_id'] = "%s_%d" % (self.id_prefix,
self.object.id)
return context
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(ArticleStatLast, self).dispatch(*args, **kwargs)