# -*- 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)