491 lines
17 KiB
Python
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)
|