parent
1ddd34d50e
commit
bdbb252a05
2 changed files with 406 additions and 398 deletions
|
@ -1,11 +1,44 @@
|
|||
# -*- 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)
|
||||
|
||||
french_days = {
|
||||
1: "lundi",
|
||||
2: "mardi",
|
||||
3: "mercredi",
|
||||
4: "jeudi",
|
||||
5: "vendredi",
|
||||
6: "samedi",
|
||||
7: "dimanche",
|
||||
}
|
||||
|
||||
french_months = {
|
||||
1: "janvier",
|
||||
2: "février",
|
||||
3: "mars",
|
||||
4: "avril",
|
||||
5: "mai",
|
||||
6: "juin",
|
||||
7: "juillet",
|
||||
8: "août",
|
||||
9: "septembre",
|
||||
10: "octobre",
|
||||
11: "novembre",
|
||||
12: "décembre",
|
||||
}
|
||||
|
||||
|
||||
def dayname(date):
|
||||
return french_days[date.isoweekday()]
|
||||
|
||||
|
||||
def weekname(date):
|
||||
(_, a, _) = date.isocalendar()
|
||||
week_num = a
|
||||
return "semaine %d" % week_num
|
||||
|
||||
|
||||
def monthname(date):
|
||||
return french_months[date.month]
|
||||
|
||||
|
||||
# Pareil mais pour une liste de dates
|
||||
|
@ -13,7 +46,7 @@ from kfet.models import (Account, Article, Operation, OperationGroup, Transfer)
|
|||
def daynames(dates):
|
||||
names = {}
|
||||
for i in dates:
|
||||
names[i] = dates[i].strftime("%A")
|
||||
names[i] = dayname(dates[i])
|
||||
return names
|
||||
|
||||
|
||||
|
@ -22,7 +55,7 @@ def daynames(dates):
|
|||
def weeknames(dates):
|
||||
names = {}
|
||||
for i in dates:
|
||||
names[i] = "Semaine " + dates[i].strftime("%W")
|
||||
names[i] = weekname(dates[i])
|
||||
return names
|
||||
|
||||
|
||||
|
@ -31,7 +64,7 @@ def weeknames(dates):
|
|||
def monthnames(dates):
|
||||
names = {}
|
||||
for i in dates:
|
||||
names[i] = dates[i].strftime("%B")
|
||||
names[i] = monthname(dates[i])
|
||||
return names
|
||||
|
||||
|
||||
|
@ -102,389 +135,3 @@ def tot_ventes(queryset):
|
|||
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)
|
||||
|
|
367
kfet/views.py
367
kfet/views.py
|
@ -8,6 +8,8 @@ from django.shortcuts import render, get_object_or_404, redirect
|
|||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.cache import cache
|
||||
from django.views.generic import ListView, DetailView
|
||||
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
|
||||
from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.contrib import messages
|
||||
|
@ -39,9 +41,7 @@ import statistics
|
|||
from .statistic import daynames, monthnames, weeknames, \
|
||||
lastdays, lastweeks, lastmonths, \
|
||||
this_morning, this_monday_morning, this_first_month_day, \
|
||||
tot_ventes, \
|
||||
HybridDetailView, ObjectResumeStat, \
|
||||
ArticleStatLast, AccountStatBalance, AccountStatLast
|
||||
tot_ventes
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
|
@ -1957,6 +1957,103 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
|
|||
# ==========
|
||||
# Statistics
|
||||
# ==========
|
||||
|
||||
# ---------------
|
||||
# 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
|
||||
|
||||
|
||||
# Rend un DetailView en html sauf si on lui précise dans
|
||||
# l'appel à get que l'on veut un json auquel cas il en rend un
|
||||
class HybridDetailView(JSONResponseMixin,
|
||||
SingleObjectTemplateResponseMixin,
|
||||
BaseDetailView):
|
||||
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)
|
||||
|
||||
|
||||
# Rend un ListView en html sauf si on lui précise dans
|
||||
# l'appel à get que l'on veut un json auquel cas il en rend un
|
||||
class HybridListView(JSONResponseMixin,
|
||||
MultipleObjectTemplateResponseMixin,
|
||||
BaseListView):
|
||||
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)
|
||||
|
||||
|
||||
# Un résume des toutes les vues de stat d'un objet
|
||||
# NE REND PAS DE JSON
|
||||
class ObjectResumeStat(DetailView):
|
||||
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
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Evolution Balance perso
|
||||
# -----------------------
|
||||
|
@ -1996,6 +2093,122 @@ class AccountStatBalanceAll(ObjectResumeStat):
|
|||
return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
# Rend un graphe (ou un json) de l'évolution de la balance personelle
|
||||
# entre begin_date et end_date
|
||||
# prend en compte les opérations et les transferts
|
||||
# ne prend pas en compte les balance offset (TODO?)
|
||||
class AccountStatBalance(HybridDetailView):
|
||||
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)
|
||||
|
||||
|
||||
# Rend l'évolution de la balance perso de ces 30 derniers jours
|
||||
class AccountStatBalanceMonth(AccountStatBalance):
|
||||
begin_date = this_morning() - timezone.timedelta(days=30)
|
||||
|
@ -2062,6 +2275,73 @@ class AccountStatLastAll(ObjectResumeStat):
|
|||
return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class AccountStatLast(HybridDetailView):
|
||||
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)
|
||||
|
||||
|
||||
# Rend les achats pour ce compte des 7 derniers jours
|
||||
|
@ -2133,6 +2413,87 @@ class ArticleStatLastAll(ObjectResumeStat):
|
|||
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
# 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):
|
||||
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)
|
||||
|
||||
|
||||
# Rend les ventes des 7 derniers jours
|
||||
# Aujourd'hui non compris
|
||||
class ArticleStatLastDay(ArticleStatLast):
|
||||
|
|
Loading…
Reference in a new issue