From 3a7ffefacf634fa6ccb824984a96fbd4d29c77df Mon Sep 17 00:00:00 2001 From: Qwann Date: Fri, 9 Dec 2016 21:45:34 +0100 Subject: [PATCH] week & day stat --- kfet/statistic.py | 130 ++++++++++++++++++++++ kfet/templates/kfet/article_stat.html | 88 +++++++++++++++ kfet/urls.py | 8 ++ kfet/views.py | 154 +++++++++++++++++++++++++- 4 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 kfet/statistic.py create mode 100644 kfet/templates/kfet/article_stat.html diff --git a/kfet/statistic.py b/kfet/statistic.py new file mode 100644 index 00000000..31db25e4 --- /dev/null +++ b/kfet/statistic.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +from django.utils import timezone + +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 +# dans un dico ordonné +def daynames(dates): + names = {} + for i in dates: + names[i] = dayname(dates[i]) + return names + + +# Pareil mais pour une liste de dates +# dans un dico ordonné +def weeksnames(dates): + names = {} + for i in dates: + names[i] = weekname(dates[i]) + return names + + +# Pareil mais pour une liste de dates +# dans un dico ordonné +def monthnames(dates): + names = {} + for i in dates: + names[i] = monthname(dates[i]) + 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() +# fisrt_days = {} +# for i in range(1, nb+1): +# days[i] = + + +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 diff --git a/kfet/templates/kfet/article_stat.html b/kfet/templates/kfet/article_stat.html new file mode 100644 index 00000000..e60c4a90 --- /dev/null +++ b/kfet/templates/kfet/article_stat.html @@ -0,0 +1,88 @@ + +{% load staticfiles %} + + + + +{% comment %} +TODO : supprimer les fichiers script jQuery et Chart.js +{% endcomment %} + + + + diff --git a/kfet/urls.py b/kfet/urls.py index 9b9ebf21..ad460f19 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -121,6 +121,14 @@ urlpatterns = [ teamkfet_required(views.ArticleUpdate.as_view()), name = 'kfet.article.update'), + # Article - Statistics + url('^articles/(?P\d+)/stat/week$', + views.ArticleStatWeek.as_view(), + name = 'kfet.article.stats.week'), + url('^articles/(?P\d+)/stat/day$', + views.ArticleStatDay.as_view(), + name = 'kfet.article.stats.day'), + # ----- # K-Psul urls # ----- diff --git a/kfet/views.py b/kfet/views.py index f95fb2c6..e973d27b 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -8,6 +8,7 @@ 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.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 @@ -26,7 +27,7 @@ from gestioncof.models import CofProfile, Clipper from kfet.decorators import teamkfet_required from kfet.models import (Account, Checkout, Article, Settings, AccountNegative, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, - InventoryArticle, Order, OrderArticle) + InventoryArticle, Order, OrderArticle, Operation) from kfet.forms import * from collections import defaultdict from kfet import consumers @@ -35,10 +36,12 @@ import django_cas_ng import hashlib import heapq import statistics +from .statistic import lastdays, daynames, this_morning,\ + tot_ventes, weeksnames, this_monday_morning, lastweeks @login_required def home(request): - return render(request, "kfet/base.html") + return render(request, "kfet/home.html") @teamkfet_required def login_genericteam(request): @@ -793,6 +796,8 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): # Updating return super(ArticleUpdate, self).form_valid(form) + + # ----- # K-Psul # ----- @@ -1941,3 +1946,148 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): return self.form_invalid(form) # Updating return super(SupplierUpdate, self).form_valid(form) + + +# ----- +# 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) + + +# Article Statistiques + + +class ArticleStat(HybridDetailView): + model = Article + template_name = 'kfet/article_stat.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(ArticleStat, 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_%s" % (self.id_prefix, + self.object.name) + return context + + +class ArticleStatDay(ArticleStat): + end_date = this_morning() + id_prefix = "last_week" + + def get_dates(self, **kwargs): + return lastdays(7) + + def get_labels(self, **kwargs): + days = lastdays(7) + return daynames(days) + + +class ArticleStatWeek(ArticleStat): + end_date = this_monday_morning() + id_prefix = "last_weeks" + + def get_dates(self, **kwargs): + return lastweeks(7) + + def get_labels(self, **kwargs): + weeks = lastweeks(7) + return weeksnames(weeks)