cleaning: PEP8, html, permissions

This commit is contained in:
Qwann 2016-12-24 12:33:04 +01:00
parent 7070129add
commit de9387c6ad
9 changed files with 231 additions and 78 deletions

View file

@ -505,6 +505,10 @@ class OperationGroup(models.Model):
related_name = "+", related_name = "+",
blank = True, null = True, default = None) blank = True, null = True, default = None)
def __str__(self):
return ', '.join(map(str, self.opes.all()))
class Operation(models.Model): class Operation(models.Model):
PURCHASE = 'purchase' PURCHASE = 'purchase'
DEPOSIT = 'deposit' DEPOSIT = 'deposit'
@ -549,6 +553,18 @@ class Operation(models.Model):
max_digits = 6, decimal_places = 2, max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None) blank = True, null = True, default = None)
def __str__(self):
templates = {
self.PURCHASE: "{nb} {article.name} ({amount}€)",
self.DEPOSIT: "charge ({amount})",
self.WITHDRAW: "retrait ({amount})",
self.INITIAL: "initial ({amount})",
}
return templates[self.type].format(nb=self.article_nb,
article=self.article,
amount=self.amount)
class GlobalPermissions(models.Model): class GlobalPermissions(models.Model):
class Meta: class Meta:
managed = False managed = False

View file

@ -364,3 +364,11 @@ textarea {
.help h4 { .help h4 {
margin:15px 0; margin:15px 0;
} }
/*
* Statistiques
*/
.stat-graph {
height: 100px;
}

View file

@ -15,8 +15,11 @@
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {
var stat_last = $("#stat_last"); var stat_last = $("#stat_last");
var stat_balance = $("#stat_balance");
var stat_last_url = "{% url 'kfet.account.stat.last' trigramme=account.trigramme %}"; var stat_last_url = "{% url 'kfet.account.stat.last' trigramme=account.trigramme %}";
var stat_balance_url = "{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}";
get_thing(stat_last_url, stat_last, "Stat non trouvées :("); get_thing(stat_last_url, stat_last, "Stat non trouvées :(");
get_thing(stat_balance_url, stat_balance, "Stat non trouvées :(");
// FONCTIONS // FONCTIONS
// Permet de raffraichir un champ, étant donné : // Permet de raffraichir un champ, étant donné :
// thing_url : l'url contenant le contenu // thing_url : l'url contenant le contenu
@ -76,13 +79,21 @@ jQuery(document).ready(function() {
</div> </div>
{% endif %} {% endif %}
{% if account.user == request.user %} {% if account.user == request.user %}
<div class="content-right-block"> <div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2> <h2>Statistiques</h2>
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-6 nopadding"> <div class="col-sm-12 nopadding">
<div class="panel-md-margin">
<h3>Ma balance</h3>
<div id="stat_balance" class"stat-graph"></div>
</div>
</div>
</div><!-- /row -->
<div class="row">
<div class="col-sm-12 nopadding">
<div class="panel-md-margin"> <div class="panel-md-margin">
<h3>Ma consommation</h3> <h3>Ma consommation</h3>
<div id="stat_last"></div> <div id="stat_last" class"stat-graph"></div>
</div> </div>
</div> </div>
</div><!-- /row --> </div><!-- /row -->

View file

@ -1,7 +1,20 @@
<!doctype html> <!doctype html>
{% load dictionary_extras %}
<body> <body>
<canvas id="{{ chart_id }}"></canvas> <canvas id="{{ chart_id }}" height="100"></canvas>
{% comment %}
<ul>
{% for change in changes %}
<li>
{{ change | get_item:'label'}}
| {{ change | get_item:'at'}}
| ({{ change | get_item:'amount'}})
| balance {{ change | get_item:'balance'}}
</li>
{% endfor %}
</ul>
{% endcomment %}
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {
@ -9,29 +22,21 @@ jQuery(document).ready(function() {
var myChart = new Chart(ctx1, { var myChart = new Chart(ctx1, {
type: 'line', type: 'line',
data: { data: {
labels: [
{% for k,label in labels.items %}
{% if forloop.last %}
"{{ label }}"
{% else %}
"{{ label }}",
{% endif %}
{% endfor %}
],
datasets: [{ datasets: [{
label: 'Nb items achetés', label: 'Balance',
borderColor: 'rgb(255, 99, 132)', borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.5)',
data: [ data: [
{% for k,nb in nb_ventes.items %} {% for change in changes %}
{% if forloop.last %} {
"{{ nb }}" x: new Date("{{ change | get_item:'at'}}"),
{% else %} y: {{ change | get_item:'balance'| stringformat:"f" }},
"{{ nb }}", label: "{{change|get_item:'label'}}"
{% endif %} }{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
], ],
fill: false, fill: true,
steppedLine: true,
lineTension: 0, lineTension: 0,
}] }]
}, },
@ -45,6 +50,40 @@ jQuery(document).ready(function() {
mode: 'nearest', mode: 'nearest',
intersect: false, intersect: false,
}, },
scales: {
xAxes: [{
type: "time",
display: true,
scaleLabel: {
display: false,
labelString: 'Date'
},
time: {
tooltipFormat: 'll HH:mm',
min: new Date("{{ min_date }}"),
max: new Date("{{ max_date }}"),
displayFormats: {
'millisecond': 'SSS [ms]',
'second': 'mm:ss a',
'minute': 'DD MMM',
'hour': 'ddd h[h]',
'day': 'DD MMM',
'week': 'DD MMM',
'month': 'MMM',
'quarter': 'MMM',
'year': 'YYYY',
}
}
}],
yAxes: [{
display: true,
scaleLabel: {
display: false,
labelString: 'value'
}
}]
}
} }
}); });
}); });

View file

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<body> <body>
<canvas id="{{ chart_id }}"></canvas> <canvas id="{{ chart_id }}" height="100" ></canvas>
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {

View file

@ -78,9 +78,11 @@
</table> </table>
</div> </div>
</div><!-- /row--> </div><!-- /row-->
</div>
<div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2> <h2>Statistiques</h2>
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-6 nopadding"> <div class="col-sm-12 nopadding">
<div class="panel-md-margin"> <div class="panel-md-margin">
<h3>Ventes de {{ article.name }}</h3> <h3>Ventes de {{ article.name }}</h3>
<div id="stat_last"></div> <div id="stat_last"></div>

View file

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<body> <body>
<canvas id="{{ chart_id }}"></canvas> <canvas id="{{ chart_id }}" height="100" ></canvas>
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {

View file

@ -5,27 +5,47 @@
<div class="btn-group btn-group-justified" role="group" aria-label="select-period"> <div class="btn-group btn-group-justified" role="group" aria-label="select-period">
{% for k,stat in stats.items %} {% for k,stat in stats.items %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="{{ stat | get_item:'btn' }}" type="button" class="btn btn-primary">{{ stat | get_item:'label' }}</button> <button id="{{ stat | get_item:'btn' }}" type="button" class="btn btn-primary {{ id_prefix }}-btn {%if k == default_stat%} focus{%endif%}">{{ stat | get_item:'label' }}</button>
</div> </div>
{% endfor %} {% endfor %}
</div><!-- /boutons --> </div><!-- /boutons -->
<div id="{{ content_id}}"> <div id="{{ content_id}}">
</div> </div>
<script>
jQuery(document).ready(function() {
// FONCTIONS
// Permet de raffraichir un champ, étant donné :
// thing_url : l'url contenant le contenu
// thing_div : le div où le mettre
// empty_... : le truc à dire si on a un contenu vide
function get_thing(thing_url, thing_div, empty_thing_message) {
$.get(thing_url, function(data) {
if(jQuery.trim(data).length==0) {
thing_div.html(empty_thing_message);
} else {
thing_div.html(data);
}
});
}
});
</script>
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {
// VARIABLES // VARIABLES
// défaut {{id_prefix}}_content_id = $("#{{content_id}}");
{{if_prefix}}_content_id = $("#{{content_id}}"); {{id_prefix}}_btns = $(".{{id_prefix}}-btn");
{% for k,stat in stats.items %} {% for k,stat in stats.items %}
{% if k == default_stat %} {% if k == default_stat %}
{{id_prefix}}_default_url = "{{ stat | get_item:'url' }}"; {{id_prefix}}_default_url = "{{ stat | get_item:'url' }}";
{% endif %} {% endif %}
{% endfor %} {% endfor %}
// INIT // INIT
get_thing({{id_prefix}}_default_url, {{if_prefix}}_content_id, "Ouppss ?"); get_thing({{id_prefix}}_default_url, {{id_prefix}}_content_id, "Ouppss ?");
{% for k,stat in stats.items %} {% for k,stat in stats.items %}
$("#{{stat|get_item:'btn'}}").on('click', function() { $("#{{stat|get_item:'btn'}}").on('click', function() {
get_thing("{{stat|get_item:'url'}}", {{if_prefix}}_content_id, "Ouuups ?") get_thing("{{stat|get_item:'url'}}", {{id_prefix}}_content_id, "Ouuups ?");
{{id_prefix}}_btns.removeClass("focus");
$("#{{stat|get_item:'btn'}}").addClass("focus");
}); });
{% endfor %} {% endfor %}
// FONCTIONS // FONCTIONS

View file

@ -24,11 +24,12 @@ from django.db.models import F, Sum, Prefetch, Count, Func
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from gestioncof.models import CofProfile, Clipper 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, Operation) InventoryArticle, Order, OrderArticle, Operation, OperationGroup, Transfer)
from kfet.forms import * from kfet.forms import *
from collections import defaultdict from collections import defaultdict
from kfet import consumers from kfet import consumers
@ -2087,18 +2088,23 @@ class AccountStatBalanceAll(ObjectResumeStat):
def get_object_url_kwargs(self, **kwargs): def get_object_url_kwargs(self, **kwargs):
return {'trigramme': self.object.trigramme} return {'trigramme': self.object.trigramme}
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
# Rend un graphe (ou un json) de l'évolution de la balance personelle # Rend un graphe (ou un json) de l'évolution de la balance personelle
# entre begin_date et end_date # entre begin_date et end_date
# prend en compte les opérations et les transferts # prend en compte les opérations et les transferts
# ne prend pas en compte les autorisations de négatif (TODO?) # ne prend pas en compte les balance offset (TODO?)
class AccountStatBalance(HybridDetailView): class AccountStatBalance(HybridDetailView):
model = Account model = Account
trigramme_url_kwarg = 'trigramme' trigramme_url_kwarg = 'trigramme'
template_name = 'kfet/account_stat_balance.html' template_name = 'kfet/account_stat_balance.html'
context_object_name = 'account' context_object_name = 'account'
begin_date = this_morning() begin_date = this_morning()
end_date = timezone.now() end_date = timezone.now() # ne gère pas encore autre chose que now
anytime = False # un cas particulier
id_prefix = "lol" id_prefix = "lol"
def get_object(self, **kwargs): def get_object(self, **kwargs):
@ -2108,25 +2114,23 @@ class AccountStatBalance(HybridDetailView):
def get_changes_list(self, **kwargs): def get_changes_list(self, **kwargs):
account = self.object account = self.object
# On récupère les opérations # On récupère les opérations
# TODO: retirer les opgroup dont tous les op sont annulées
opgroups = list(OperationGroup.objects opgroups = list(OperationGroup.objects
.filter(on_acc=account) .filter(on_acc=account)
.filter(at__gte=self.begin_date) .filter(at__gte=self.begin_date)
.filter(at__lte=self.end_date) .filter(at__lte=self.end_date))
)
# On récupère les transferts reçus # On récupère les transferts reçus
received_transfers = list(Transfer.objects received_transfers = list(Transfer.objects
.filter(to_acc=account) .filter(to_acc=account)
.filter(canceled_at=None) .filter(canceled_at=None)
.filter(transfers__at__gte=self.begin_date) .filter(group__at__gte=self.begin_date)
.filter(transfers__at__lte=self.end_date) .filter(group__at__lte=self.end_date))
)
# On récupère les transferts émis # On récupère les transferts émis
emitted_transfers = list(Transfer.objects emitted_transfers = list(Transfer.objects
.filter(to_acc=account) .filter(from_acc=account)
.filter(canceled_at=None) .filter(canceled_at=None)
.filter(transfers__at__gte=self.begin_date) .filter(group__at__gte=self.begin_date)
.filter(transfers__at__lte=self.end_date) .filter(group__at__lte=self.end_date))
)
# On transforme tout ça en une liste de dictionnaires sous la forme # On transforme tout ça en une liste de dictionnaires sous la forme
# {'at': date, # {'at': date,
# 'amount': changement de la balance (négatif si diminue la balance, # 'amount': changement de la balance (négatif si diminue la balance,
@ -2136,46 +2140,74 @@ class AccountStatBalance(HybridDetailView):
# sera mis à jour lors d'une # sera mis à jour lors d'une
# autre passe) # autre passe)
# } # }
actions=[] actions = [
for op in opgroups: # Maintenant (à changer si on gère autre chose que now)
action = { {
'at': op.at, 'at': self.end_date.isoformat(),
'amount': op.amount, 'amout': 0,
'label': "opération", #TODO '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, 'balance': 0,
} }
actions.append(action) ]
for tr in received_transfers:
action = {
'at': tr.transfers.at,
'amount': re.amount,
'label': "Transfert", #TODO
'balance': 0,
}
actions.append(action)
for tr in emitted_transfers:
action = {
'at': tr.transfers.at,
'amount': -re.amount,
'label': "Transfert", #TODO
'balance': 0,
}
actions.append(action)
# Maintenant on trie la liste des actions par ordre du plus récent # Maintenant on trie la liste des actions par ordre du plus récent
# an plus ancien et on met à jour la balance # an plus ancien et on met à jour la balance
actions = sorted(actions, key=lambda k: k['at'], reverse=True) actions = sorted(actions, key=lambda k: k['at'], reverse=True)
actions[0]['balance'] = account.balance actions[0]['balance'] = account.balance
for i in range(len(actions)-1): for i in range(len(actions)-1):
actions[i+1]['balance'] = actions[i]['balance'] - actions[i+1]['amount'] actions[i+1]['balance'] = actions[i]['balance'] \
- actions[i+1]['amount']
return actions return actions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = {} context = {}
changes = self.get_changes_list() changes = self.get_changes_list()
context['changes'] = changes 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 # TODO: offset
return context 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 # Rend l'évolution de la balance perso de ces 30 derniers jours
class AccountStatBalanceMonth(AccountStatBalance): class AccountStatBalanceMonth(AccountStatBalance):
@ -2205,10 +2237,11 @@ class AccountStatBalanceYear(AccountStatBalance):
class AccountStatBalanceAnytime(AccountStatBalance): class AccountStatBalanceAnytime(AccountStatBalance):
begin_date = timezone.datetime(year=1980, month=1, day=1) begin_date = timezone.datetime(year=1980, month=1, day=1)
id_prefix = ID_PREFIX_ACC_BALANCE_ANYTIME id_prefix = ID_PREFIX_ACC_BALANCE_ANYTIME
anytime = True
# ------------------------ # ------------------------
# Consommation personnelle # Consommation personnelle
# ------------------------ # ------------------------
ID_PREFIX_ACC_LAST = "last_acc" ID_PREFIX_ACC_LAST = "last_acc"
ID_PREFIX_ACC_LAST_DAYS = "last_days_acc" ID_PREFIX_ACC_LAST_DAYS = "last_days_acc"
@ -2237,6 +2270,10 @@ class AccountStatLastAll(ObjectResumeStat):
def get_object_url_kwargs(self, **kwargs): def get_object_url_kwargs(self, **kwargs):
return {'trigramme': self.object.trigramme} return {'trigramme': self.object.trigramme}
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
class AccountStatLast(HybridDetailView): class AccountStatLast(HybridDetailView):
model = Account model = Account
@ -2302,6 +2339,10 @@ class AccountStatLast(HybridDetailView):
self.object.id) self.object.id)
return context 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 # Rend les achats pour ce compte des 7 derniers jours
# Aujourd'hui non compris # Aujourd'hui non compris
@ -2367,6 +2408,10 @@ class ArticleStatLastAll(ObjectResumeStat):
'kfet.article.stat.last.week', 'kfet.article.stat.last.week',
'kfet.article.stat.last.day'] 'kfet.article.stat.last.day']
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
# Rend un graph des ventes sur une plage de temps à préciser. # Rend un graph des ventes sur une plage de temps à préciser.
# Le graphique distingue les ventes sur LIQ et sur les autres trigrammes # Le graphique distingue les ventes sur LIQ et sur les autres trigrammes
@ -2444,6 +2489,10 @@ class ArticleStatLast(HybridDetailView):
self.object.id) self.object.id)
return context 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 # Rend les ventes des 7 derniers jours
# Aujourd'hui non compris # Aujourd'hui non compris
@ -2472,6 +2521,7 @@ class ArticleStatLastWeek(ArticleStatLast):
weeks = lastweeks(7) weeks = lastweeks(7)
return weeknames(weeks) return weeknames(weeks)
# Rend les ventes des 7 derniers mois # Rend les ventes des 7 derniers mois
# Le mois en cours n'est pas compris # Le mois en cours n'est pas compris
class ArticleStatLastMonth(ArticleStatLast): class ArticleStatLastMonth(ArticleStatLast):
@ -2489,9 +2539,10 @@ class ArticleStatLastMonth(ArticleStatLast):
# Article Statistique Catégories # Article Statistique Catégories
# ------------------------------ # ------------------------------
class DurationStat(HybridListView): class DurationStat(HybridListView):
lookup_duration_type = 'day' # 'day' || 'week' || 'month' lookup_duration_type = 'day' # 'day' || 'week' || 'month'
lookup_duration_number = 3 # ie ici : 3 jours lookup_duration_number = 3 # ie ici : 3 jours
def get_end_date(self, **kwargs): def get_end_date(self, **kwargs):
if self.lookup_duration_type == 'day': if self.lookup_duration_type == 'day':
@ -2500,7 +2551,8 @@ class DurationStat(HybridListView):
return this_monday_morning() return this_monday_morning()
elif self.lookup_duration_type == 'month': elif self.lookup_duration_type == 'month':
return this_first_month_day() return this_first_month_day()
else: raise ValueError('duration_type invalid') else:
raise ValueError('duration_type invalid')
def get_begining_date(self, **kwargs): def get_begining_date(self, **kwargs):
end_date = self.get_end_date(self, **kwargs) end_date = self.get_end_date(self, **kwargs)
@ -2510,18 +2562,23 @@ class DurationStat(HybridListView):
days = 7*self.lookup_nb_duration days = 7*self.lookup_nb_duration
elif self.lookup_duration_type == 'month': elif self.lookup_duration_type == 'month':
days = 30*self.lookup_nb_duration days = 30*self.lookup_nb_duration
else: raise ValueError('this should not be happening.') else:
raise ValueError('this should not be happening.')
delta = timezone.timedelta(days=days) delta = timezone.timedelta(days=days)
return end_date - delta return end_date - delta
#TODO @method_decorator(login_required)
# class CategoryStatAll(DurationStat): def dispatch(self, *args, **kwargs):
return super(DurationStat, self).dispatch(*args, **kwargs)
# TODO
# class CategoryDurationStat(DurationStat):
# model = ArticleCategory # model = ArticleCategory
# template_name = 'kfet/category_stat.html' # template_name = 'kfet/category_stat.html'
# #
# def get_context_data(self, **kwargs): # def get_context_data(self, **kwargs):
# context = {} # context = {}
# queryset = kwargs.pop('object_list', self.object_list) # queryset = kwargs.pop('object_list', self.object_list)
# #
# return context # return context