forked from DGNum/gestioCOF
week & day stat
This commit is contained in:
parent
e4c8209df8
commit
3a7ffefacf
4 changed files with 378 additions and 2 deletions
130
kfet/statistic.py
Normal file
130
kfet/statistic.py
Normal file
|
@ -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
|
88
kfet/templates/kfet/article_stat.html
Normal file
88
kfet/templates/kfet/article_stat.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<!doctype html>
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<canvas id="{{ chart_id }}"></canvas>
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
TODO : supprimer les fichiers script jQuery et Chart.js
|
||||||
|
{% endcomment %}
|
||||||
|
<script src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function() {
|
||||||
|
var ctx1 = $({{ chart_id }});
|
||||||
|
var myChart = new Chart(ctx1, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [
|
||||||
|
{% for k,label in labels.items %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
"{{ label }}"
|
||||||
|
{% else %}
|
||||||
|
"{{ label }}",
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Toutes consomations',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
backgroundColor: 'rgb(255, 99, 132)',
|
||||||
|
data: [
|
||||||
|
{% for k,nb in nb_ventes.items %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
"{{ nb }}"
|
||||||
|
{% else %}
|
||||||
|
"{{ nb }}",
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
fill: false,
|
||||||
|
lineTension: 0,
|
||||||
|
} , {
|
||||||
|
label: 'LIQ',
|
||||||
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
backgroundColor: 'rgb(54, 162, 235)',
|
||||||
|
data: [
|
||||||
|
{% for k,nb in nb_liq.items %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
"{{ nb }}"
|
||||||
|
{% else %}
|
||||||
|
"{{ nb }}",
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
fill: false,
|
||||||
|
lineTension: 0,
|
||||||
|
} , {
|
||||||
|
label: 'Comptes K-Fêt',
|
||||||
|
borderColor: 'rgb(255, 205, 86)',
|
||||||
|
backgroundColor: 'rgb(255, 205, 86)',
|
||||||
|
data: [
|
||||||
|
{% for k,nb in nb_accounts.items %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
"{{ nb }}"
|
||||||
|
{% else %}
|
||||||
|
"{{ nb }}",
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
fill: false,
|
||||||
|
lineTension: 0,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
tooltips: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
|
@ -121,6 +121,14 @@ urlpatterns = [
|
||||||
teamkfet_required(views.ArticleUpdate.as_view()),
|
teamkfet_required(views.ArticleUpdate.as_view()),
|
||||||
name = 'kfet.article.update'),
|
name = 'kfet.article.update'),
|
||||||
|
|
||||||
|
# Article - Statistics
|
||||||
|
url('^articles/(?P<pk>\d+)/stat/week$',
|
||||||
|
views.ArticleStatWeek.as_view(),
|
||||||
|
name = 'kfet.article.stats.week'),
|
||||||
|
url('^articles/(?P<pk>\d+)/stat/day$',
|
||||||
|
views.ArticleStatDay.as_view(),
|
||||||
|
name = 'kfet.article.stats.day'),
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# K-Psul urls
|
# K-Psul urls
|
||||||
# -----
|
# -----
|
||||||
|
|
154
kfet/views.py
154
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.exceptions import PermissionDenied, ValidationError
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.views.generic import ListView, DetailView
|
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.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
@ -26,7 +27,7 @@ from gestioncof.models import CofProfile, Clipper
|
||||||
from kfet.decorators import teamkfet_required
|
from kfet.decorators import teamkfet_required
|
||||||
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
|
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
|
||||||
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
|
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
|
||||||
InventoryArticle, Order, OrderArticle)
|
InventoryArticle, Order, OrderArticle, Operation)
|
||||||
from kfet.forms import *
|
from kfet.forms import *
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from kfet import consumers
|
from kfet import consumers
|
||||||
|
@ -35,10 +36,12 @@ import django_cas_ng
|
||||||
import hashlib
|
import hashlib
|
||||||
import heapq
|
import heapq
|
||||||
import statistics
|
import statistics
|
||||||
|
from .statistic import lastdays, daynames, this_morning,\
|
||||||
|
tot_ventes, weeksnames, this_monday_morning, lastweeks
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
return render(request, "kfet/base.html")
|
return render(request, "kfet/home.html")
|
||||||
|
|
||||||
@teamkfet_required
|
@teamkfet_required
|
||||||
def login_genericteam(request):
|
def login_genericteam(request):
|
||||||
|
@ -793,6 +796,8 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
|
||||||
# Updating
|
# Updating
|
||||||
return super(ArticleUpdate, self).form_valid(form)
|
return super(ArticleUpdate, self).form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# K-Psul
|
# K-Psul
|
||||||
# -----
|
# -----
|
||||||
|
@ -1941,3 +1946,148 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
# Updating
|
# Updating
|
||||||
return super(SupplierUpdate, self).form_valid(form)
|
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)
|
||||||
|
|
Loading…
Reference in a new issue