forked from DGNum/gestioCOF
cleaning: PEP8, html, permissions
This commit is contained in:
parent
7070129add
commit
de9387c6ad
9 changed files with 231 additions and 78 deletions
|
@ -505,6 +505,10 @@ class OperationGroup(models.Model):
|
|||
related_name = "+",
|
||||
blank = True, null = True, default = None)
|
||||
|
||||
def __str__(self):
|
||||
return ', '.join(map(str, self.opes.all()))
|
||||
|
||||
|
||||
class Operation(models.Model):
|
||||
PURCHASE = 'purchase'
|
||||
DEPOSIT = 'deposit'
|
||||
|
@ -549,6 +553,18 @@ class Operation(models.Model):
|
|||
max_digits = 6, decimal_places = 2,
|
||||
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 Meta:
|
||||
managed = False
|
||||
|
|
|
@ -364,3 +364,11 @@ textarea {
|
|||
.help h4 {
|
||||
margin:15px 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Statistiques
|
||||
*/
|
||||
|
||||
.stat-graph {
|
||||
height: 100px;
|
||||
}
|
||||
|
|
|
@ -15,8 +15,11 @@
|
|||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
var stat_last = $("#stat_last");
|
||||
var stat_balance = $("#stat_balance");
|
||||
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_balance_url, stat_balance, "Stat non trouvées :(");
|
||||
// FONCTIONS
|
||||
// Permet de raffraichir un champ, étant donné :
|
||||
// thing_url : l'url contenant le contenu
|
||||
|
@ -76,13 +79,21 @@ jQuery(document).ready(function() {
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if account.user == request.user %}
|
||||
<div class="content-right-block">
|
||||
<div class="content-right-block content-right-block-transparent">
|
||||
<h2>Statistiques</h2>
|
||||
<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">
|
||||
<h3>Ma consommation</h3>
|
||||
<div id="stat_last"></div>
|
||||
<div id="stat_last" class"stat-graph"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
<!doctype html>
|
||||
{% load dictionary_extras %}
|
||||
|
||||
<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>
|
||||
jQuery(document).ready(function() {
|
||||
|
@ -9,29 +22,21 @@ jQuery(document).ready(function() {
|
|||
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: 'Nb items achetés',
|
||||
label: 'Balance',
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.5)',
|
||||
data: [
|
||||
{% for k,nb in nb_ventes.items %}
|
||||
{% if forloop.last %}
|
||||
"{{ nb }}"
|
||||
{% else %}
|
||||
"{{ nb }}",
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for change in changes %}
|
||||
{
|
||||
x: new Date("{{ change | get_item:'at'}}"),
|
||||
y: {{ change | get_item:'balance'| stringformat:"f" }},
|
||||
label: "{{change|get_item:'label'}}"
|
||||
}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
fill: false,
|
||||
fill: true,
|
||||
steppedLine: true,
|
||||
lineTension: 0,
|
||||
}]
|
||||
},
|
||||
|
@ -45,6 +50,40 @@ jQuery(document).ready(function() {
|
|||
mode: 'nearest',
|
||||
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'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
|
||||
<body>
|
||||
<canvas id="{{ chart_id }}"></canvas>
|
||||
<canvas id="{{ chart_id }}" height="100" ></canvas>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
|
|
|
@ -78,9 +78,11 @@
|
|||
</table>
|
||||
</div>
|
||||
</div><!-- /row-->
|
||||
</div>
|
||||
<div class="content-right-block content-right-block-transparent">
|
||||
<h2>Statistiques</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ventes de {{ article.name }}</h3>
|
||||
<div id="stat_last"></div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
|
||||
<body>
|
||||
<canvas id="{{ chart_id }}"></canvas>
|
||||
<canvas id="{{ chart_id }}" height="100" ></canvas>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
|
|
|
@ -5,27 +5,47 @@
|
|||
<div class="btn-group btn-group-justified" role="group" aria-label="select-period">
|
||||
{% for k,stat in stats.items %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div><!-- /boutons -->
|
||||
<div id="{{ content_id}}">
|
||||
</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>
|
||||
jQuery(document).ready(function() {
|
||||
// VARIABLES
|
||||
// défaut
|
||||
{{if_prefix}}_content_id = $("#{{content_id}}");
|
||||
{{id_prefix}}_content_id = $("#{{content_id}}");
|
||||
{{id_prefix}}_btns = $(".{{id_prefix}}-btn");
|
||||
{% for k,stat in stats.items %}
|
||||
{% if k == default_stat %}
|
||||
{{id_prefix}}_default_url = "{{ stat | get_item:'url' }}";
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
// 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 %}
|
||||
$("#{{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 %}
|
||||
// FONCTIONS
|
||||
|
|
151
kfet/views.py
151
kfet/views.py
|
@ -24,11 +24,12 @@ from django.db.models import F, Sum, Prefetch, Count, Func
|
|||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.decorators import method_decorator
|
||||
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, Operation)
|
||||
InventoryArticle, Order, OrderArticle, Operation, OperationGroup, Transfer)
|
||||
from kfet.forms import *
|
||||
from collections import defaultdict
|
||||
from kfet import consumers
|
||||
|
@ -2087,18 +2088,23 @@ class AccountStatBalanceAll(ObjectResumeStat):
|
|||
def get_object_url_kwargs(self, **kwargs):
|
||||
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
|
||||
# entre begin_date et end_date
|
||||
# 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):
|
||||
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()
|
||||
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):
|
||||
|
@ -2108,25 +2114,23 @@ class AccountStatBalance(HybridDetailView):
|
|||
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)
|
||||
)
|
||||
.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(transfers__at__gte=self.begin_date)
|
||||
.filter(transfers__at__lte=self.end_date)
|
||||
)
|
||||
.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(to_acc=account)
|
||||
.filter(from_acc=account)
|
||||
.filter(canceled_at=None)
|
||||
.filter(transfers__at__gte=self.begin_date)
|
||||
.filter(transfers__at__lte=self.end_date)
|
||||
)
|
||||
.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,
|
||||
|
@ -2136,46 +2140,74 @@ class AccountStatBalance(HybridDetailView):
|
|||
# sera mis à jour lors d'une
|
||||
# autre passe)
|
||||
# }
|
||||
actions=[]
|
||||
for op in opgroups:
|
||||
action = {
|
||||
'at': op.at,
|
||||
'amount': op.amount,
|
||||
'label': "opération", #TODO
|
||||
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,
|
||||
}
|
||||
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
|
||||
# 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']
|
||||
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)
|
||||
|
||||
|
||||
# Rend l'évolution de la balance perso de ces 30 derniers jours
|
||||
class AccountStatBalanceMonth(AccountStatBalance):
|
||||
|
@ -2205,10 +2237,11 @@ class AccountStatBalanceYear(AccountStatBalance):
|
|||
class AccountStatBalanceAnytime(AccountStatBalance):
|
||||
begin_date = timezone.datetime(year=1980, month=1, day=1)
|
||||
id_prefix = ID_PREFIX_ACC_BALANCE_ANYTIME
|
||||
anytime = True
|
||||
|
||||
|
||||
# ------------------------
|
||||
# Consommation personnelle
|
||||
# Consommation personnelle
|
||||
# ------------------------
|
||||
ID_PREFIX_ACC_LAST = "last_acc"
|
||||
ID_PREFIX_ACC_LAST_DAYS = "last_days_acc"
|
||||
|
@ -2237,6 +2270,10 @@ class AccountStatLastAll(ObjectResumeStat):
|
|||
def get_object_url_kwargs(self, **kwargs):
|
||||
return {'trigramme': self.object.trigramme}
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class AccountStatLast(HybridDetailView):
|
||||
model = Account
|
||||
|
@ -2302,6 +2339,10 @@ class AccountStatLast(HybridDetailView):
|
|||
self.object.id)
|
||||
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
|
||||
# Aujourd'hui non compris
|
||||
|
@ -2367,6 +2408,10 @@ class ArticleStatLastAll(ObjectResumeStat):
|
|||
'kfet.article.stat.last.week',
|
||||
'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.
|
||||
# Le graphique distingue les ventes sur LIQ et sur les autres trigrammes
|
||||
|
@ -2444,6 +2489,10 @@ class ArticleStatLast(HybridDetailView):
|
|||
self.object.id)
|
||||
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
|
||||
# Aujourd'hui non compris
|
||||
|
@ -2472,6 +2521,7 @@ class ArticleStatLastWeek(ArticleStatLast):
|
|||
weeks = lastweeks(7)
|
||||
return weeknames(weeks)
|
||||
|
||||
|
||||
# Rend les ventes des 7 derniers mois
|
||||
# Le mois en cours n'est pas compris
|
||||
class ArticleStatLastMonth(ArticleStatLast):
|
||||
|
@ -2489,9 +2539,10 @@ class ArticleStatLastMonth(ArticleStatLast):
|
|||
# Article Statistique Catégories
|
||||
# ------------------------------
|
||||
|
||||
|
||||
class DurationStat(HybridListView):
|
||||
lookup_duration_type = 'day' # 'day' || 'week' || 'month'
|
||||
lookup_duration_number = 3 # ie ici : 3 jours
|
||||
lookup_duration_type = 'day' # 'day' || 'week' || 'month'
|
||||
lookup_duration_number = 3 # ie ici : 3 jours
|
||||
|
||||
def get_end_date(self, **kwargs):
|
||||
if self.lookup_duration_type == 'day':
|
||||
|
@ -2500,7 +2551,8 @@ class DurationStat(HybridListView):
|
|||
return this_monday_morning()
|
||||
elif self.lookup_duration_type == 'month':
|
||||
return this_first_month_day()
|
||||
else: raise ValueError('duration_type invalid')
|
||||
else:
|
||||
raise ValueError('duration_type invalid')
|
||||
|
||||
def get_begining_date(self, **kwargs):
|
||||
end_date = self.get_end_date(self, **kwargs)
|
||||
|
@ -2510,18 +2562,23 @@ class DurationStat(HybridListView):
|
|||
days = 7*self.lookup_nb_duration
|
||||
elif self.lookup_duration_type == 'month':
|
||||
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)
|
||||
return end_date - delta
|
||||
|
||||
|
||||
#TODO
|
||||
# class CategoryStatAll(DurationStat):
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(DurationStat, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
# TODO
|
||||
# class CategoryDurationStat(DurationStat):
|
||||
# model = ArticleCategory
|
||||
# template_name = 'kfet/category_stat.html'
|
||||
#
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = {}
|
||||
# queryset = kwargs.pop('object_list', self.object_list)
|
||||
#
|
||||
#
|
||||
# return context
|
||||
|
|
Loading…
Reference in a new issue