Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/command_interface

This commit is contained in:
Ludovic Stephan 2017-03-05 21:12:53 -03:00
commit 61e2fedb08
17 changed files with 28734 additions and 86 deletions

View file

@ -18,6 +18,7 @@ from django.db.models import F
from django.core.cache import cache from django.core.cache import cache
from datetime import date, timedelta from datetime import date, timedelta
import re import re
import hashlib
def choices_length(choices): def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
@ -154,6 +155,7 @@ class Account(models.Model):
# - Enregistre User, CofProfile à partir de "data" # - Enregistre User, CofProfile à partir de "data"
# - Enregistre Account # - Enregistre Account
def save(self, data = {}, *args, **kwargs): def save(self, data = {}, *args, **kwargs):
if self.pk and data: if self.pk and data:
# Account update # Account update
@ -200,6 +202,11 @@ class Account(models.Model):
self.cofprofile = cof self.cofprofile = cof
super(Account, self).save(*args, **kwargs) super(Account, self).save(*args, **kwargs)
def change_pwd(self, pwd):
pwd_sha256 = hashlib.sha256(pwd.encode('utf-8'))\
.hexdigest()
self.password = pwd_sha256
# Surcharge de delete # Surcharge de delete
# Pas de suppression possible # Pas de suppression possible
# Cas à régler plus tard # Cas à régler plus tard
@ -505,6 +512,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 +560,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

@ -99,6 +99,25 @@ textarea {
font-weight:bold; font-weight:bold;
} }
.nopadding {
padding: 0 !important;
}
.panel-md-margin{
background-color: white;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 15px;
padding-top: 1px;
}
@media (min-width: 992px) {
.panel-md-margin{
margin:8px;
background-color: white;
}
}
.col-content-left, .col-content-right { .col-content-left, .col-content-right {
padding:0; padding:0;
} }
@ -165,6 +184,10 @@ textarea {
background:#fff; background:#fff;
} }
.content-right-block-transparent > div:not(.buttons-title) {
background-color: transparent;
}
.content-right-block .buttons-title { .content-right-block .buttons-title {
position:absolute; position:absolute;
top:8px; top:8px;
@ -364,3 +387,11 @@ textarea {
thead .tooltip { thead .tooltip {
font-size:13px; font-size:13px;
} }
/*
* Statistiques
*/
.stat-graph {
height: 100px;
}

View file

@ -8,7 +8,7 @@ input[type=number]::-webkit-outer-spin-button {
margin: 0; margin: 0;
} }
#account, #checkout, input, #history, #basket, #basket_rel, #previous_op, #articles_data { #account, #checkout, #article_selection, #history, #basket, #basket_rel, #previous_op, #articles_data {
background:#fff; background:#fff;
} }
@ -252,7 +252,7 @@ input[type=number]::-webkit-outer-spin-button {
width:100%; width:100%;
} }
#article_selection input { #article_selection input, #article_selection span {
height:100%; height:100%;
float:left; float:left;
border:0; border:0;
@ -263,12 +263,12 @@ input[type=number]::-webkit-outer-spin-button {
font-weight:bold; font-weight:bold;
} }
#article_selection input+input { #article_selection input+input #article_selection input+span {
border-right:0; border-right:0;
} }
#article_autocomplete { #article_autocomplete {
width:90%; width:80%;
padding-left:10px; padding-left:10px;
} }
@ -277,14 +277,24 @@ input[type=number]::-webkit-outer-spin-button {
text-align:center; text-align:center;
} }
#article_stock {
width:10%;
line-height:38px;
text-align:center;
}
@media (min-width:1200px) { @media (min-width:1200px) {
#article_autocomplete { #article_autocomplete {
width:92% width:84%
} }
#article_number { #article_number {
width:8%; width:8%;
} }
#article_stock {
width:8%;
}
} }
/* Article data */ /* Article data */
@ -319,6 +329,10 @@ input[type=number]::-webkit-outer-spin-button {
padding-left:20px; padding-left:20px;
} }
#articles_data .article.low-stock {
background:rgba(236,100,0,0.3);
}
#articles_data .article:hover { #articles_data .article:hover {
background:rgba(200,16,46,0.3); background:rgba(200,16,46,0.3);
cursor:pointer; cursor:pointer;
@ -384,6 +398,11 @@ input[type=number]::-webkit-outer-spin-button {
text-align:right; text-align:right;
} }
#basket tr .lowstock {
display:none;
padding-right:15px;
}
#basket tr.ui-selected, #basket tr.ui-selecting { #basket tr.ui-selected, #basket tr.ui-selecting {
background-color:rgba(200,16,46,0.6); background-color:rgba(200,16,46,0.6);
color:#FFF; color:#FFF;

File diff suppressed because it is too large Load diff

16
kfet/static/kfet/js/Chart.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

11557
kfet/static/kfet/js/Chart.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -37,9 +37,10 @@ function amountDisplay(amount, is_cof=false, tri='') {
return amountToUKF(amount, is_cof); return amountToUKF(amount, is_cof);
} }
function amountToUKF(amount, is_cof=false) { function amountToUKF(amount, is_cof=false, account=false) {
var rounding = account ? Math.floor : Math.round ;
var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1; var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1;
return Math.floor(amount * coef_cof * 10); return rounding(amount * coef_cof * 10);
} }
function isValidTrigramme(trigramme) { function isValidTrigramme(trigramme) {

View file

@ -0,0 +1,197 @@
(function($){
window.StatsGroup = function (url, target) {
// a class to properly display statictics
// url : points to an ObjectResumeStat that lists the options through JSON
// target : element of the DOM where to put the stats
var self = this;
var element = $(target);
var content = $("<div>");
var buttons;
function dictToArray (dict, start) {
// converts the dicts returned by JSONResponse to Arrays
// necessary because for..in does not guarantee the order
if (start === undefined) start = 0;
var array = new Array();
for (var k in dict) {
array[k] = dict[k];
}
array.splice(0, start);
return array;
}
function handleTimeChart (dict) {
// reads the balance data and put it into chartjs formatting
var data = dictToArray(dict, 0);
for (var i = 0; i < data.length; i++) {
var source = data[i];
data[i] = { x: new Date(source.at),
y: source.balance,
label: source.label }
}
return data;
}
function showStats () {
// CALLBACK : called when a button is selected
// shows the focus on the correct button
buttons.find(".focus").removeClass("focus");
$(this).addClass("focus");
// loads data and shows it
$.getJSON(this.stats_target_url + "?format=json",
displayStats);
}
function displayStats (data) {
// reads the json data and updates the chart display
var chart_datasets = [];
var charts = dictToArray(data.charts);
// are the points indexed by timestamps?
var is_time_chart = data.is_time_chart || false;
// reads the charts data
for (var i = 0; i < charts.length; i++) {
var chart = charts[i];
// format the data
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1);
chart_datasets.push(
{
label: chart.label,
borderColor: chart.color,
backgroundColor: chart.color,
fill: is_time_chart,
lineTension: 0,
data: chart_data,
steppedLine: is_time_chart,
});
}
// options for chartjs
var chart_options =
{
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'nearest',
intersect: false,
}
};
// additionnal options for time-indexed charts
if (is_time_chart) {
chart_options['scales'] = {
xAxes: [{
type: "time",
display: true,
scaleLabel: {
display: false,
labelString: 'Date'
},
time: {
tooltipFormat: 'll HH:mm',
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'
}
}]
};
}
// global object for the options
var chart_model =
{
type: 'line',
options: chart_options,
data: {
labels: dictToArray(data.labels, 1),
datasets: chart_datasets,
}
};
// saves the previous charts to be destroyed
var prev_chart = content.children();
// creates a blank canvas element and attach it to the DOM
var canvas = $("<canvas>");
content.append(canvas);
// create the chart
var chart = new Chart(canvas, chart_model);
// clean
prev_chart.remove();
}
// initialize the interface
function initialize (data) {
// creates the bar with the buttons
buttons = $("<div>",
{class: "btn-group btn-group-justified",
role: "group",
"aria-label": "select-period"});
var to_click;
var context = dictToArray(data.stats);
for (var i = 0; i < context.length; i++) {
// creates the button
var btn_wrapper = $("<div>",
{class: "btn-group",
role:"group"});
var btn = $("<button>",
{class: "btn btn-primary",
type: "button"})
.text(context[i].label)
.prop("stats_target_url", context[i].url)
.on("click", showStats);
// saves the default option to select
if (i == data.default_stat || i == 0)
to_click = btn;
// append the elements to the parent
btn_wrapper.append(btn);
buttons.append(btn_wrapper);
}
// appends the contents to the DOM
element.append(buttons);
element.append(content);
// shows the default chart
to_click.click();
};
// constructor
(function () {
$.getJSON(url + "?format=json", initialize);
})();
};
})(jQuery);

102
kfet/statistic.py Normal file
View file

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
from django.utils import timezone
from django.db.models import Sum
KFET_WAKES_UP_AT = 7
# donne le nom des jours d'une liste de dates
# dans un dico ordonné
def daynames(dates):
names = {}
for i in dates:
names[i] = dates[i].strftime("%A")
return names
# donne le nom des semaines une liste de dates
# dans un dico ordonné
def weeknames(dates):
names = {}
for i in dates:
names[i] = dates[i].strftime("Semaine %W")
return names
# donne le nom des mois d'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 - 1 - (nb - i)) % 12) + 1
year = this_year + (nb - i) // 12
first_days[i] = timezone.datetime(year=year,
month=month,
day=1,
hour=KFET_WAKES_UP_AT)
return first_days
def this_first_month_day():
now = timezone.now()
first_day = timezone.datetime(year=now.year,
month=now.month,
day=1,
hour=KFET_WAKES_UP_AT)
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,
hour=KFET_WAKES_UP_AT)
return monday_morning
def this_morning():
now = timezone.now()
morning = timezone.datetime(year=now.year,
month=now.month,
day=now.day,
hour=KFET_WAKES_UP_AT)
return morning
# Étant donné un queryset d'operations
# rend la somme des article_nb
def tot_ventes(queryset):
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
return res and res or 0

View file

@ -10,6 +10,18 @@
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
<script>
jQuery(document).ready(function() {
var stat_last = new StatsGroup("{% url 'kfet.account.stat.last' trigramme=account.trigramme %}",
$("#stat_last"));
var stat_balance = new StatsGroup("{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}",
$("#stat_balance"));
});
</script>
{% endif %}
{% endblock %} {% endblock %}
{% block title %} {% block title %}
@ -51,6 +63,27 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if account.user == request.user %}
<div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2>
<div class="row">
<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" class"stat-graph"></div>
</div>
</div>
</div><!-- /row -->
</div>
{% endif %}
<div class="content-right-block"> <div class="content-right-block">
<h2>Historique</h2> <h2>Historique</h2>
<div id="history"> <div id="history">

View file

@ -1,4 +1,5 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base.html' %}
{% load staticfiles %}
{% block title %}Informations sur l'article {{ article }}{% endblock %} {% block title %}Informations sur l'article {{ article }}{% endblock %}
{% block content-header-title %}Article - {{ article.name }}{% endblock %} {% block content-header-title %}Article - {{ article.name }}{% endblock %}
@ -76,10 +77,32 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div><!-- /row-->
</div>
<div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2>
<div class="row">
<div class="col-sm-12 nopadding">
<div class="panel-md-margin">
<h3>Ventes de {{ article.name }}</h3>
<div id="stat_last"></div>
</div>
</div>
</div><!-- /row -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> {% endblock %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
<script>
jQuery(document).ready(function() {
var stat_last = $("#stat_last");
var stat_last_url = "{% url 'kfet.article.stat.last' article.id %}";
STAT.get_thing(stat_last_url, stat_last, "Stat non trouvées :(");
});
</script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,17 @@
{% extends "kfet/base.html" %}
{% load staticfiles %}
{% block title %}Accueil{% endblock %}
{% block content-header-title %}Accueil{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-4 col-md-3 col-content-left">
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
</div>
</div>
{% endblock %}

View file

@ -123,6 +123,7 @@
<div id="article_selection"> <div id="article_selection">
<input type="text" id="article_autocomplete" autocomplete="off"> <input type="text" id="article_autocomplete" autocomplete="off">
<input type="number" id="article_number" step="1" min="1"> <input type="number" id="article_number" step="1" min="1">
<span type="stock" id="article_stock"></span>
<input type="hidden" id="article_id" value=""> <input type="hidden" id="article_id" value="">
</div> </div>
<div id="articles_data"> <div id="articles_data">
@ -221,7 +222,7 @@ $(document).ready(function() {
function displayAccountData() { function displayAccountData() {
var balance = account_data['trigramme'] != 'LIQ' ? account_data['balance'] : ''; var balance = account_data['trigramme'] != 'LIQ' ? account_data['balance'] : '';
if (balance != '') if (balance != '')
balance = amountToUKF(account_data['balance'], account_data['is_cof']); balance = amountToUKF(account_data['balance'], account_data['is_cof'], true);
var is_cof = account_data['trigramme'] ? account_data['is_cof'] : ''; var is_cof = account_data['trigramme'] ? account_data['is_cof'] : '';
if (is_cof !== '') if (is_cof !== '')
is_cof = is_cof ? '<b>COF</b>' : '<b>Non-COF</b>'; is_cof = is_cof ? '<b>COF</b>' : '<b>Non-COF</b>';
@ -616,7 +617,10 @@ $(document).ready(function() {
for (var elem in article) { for (var elem in article) {
article_html.find('.'+elem).text(article[elem]) article_html.find('.'+elem).text(article[elem])
} }
article_html.find('.price').text(amountToUKF(article['price'], false)); if (-5 <= article['stock'] && article['stock'] <= 5) {
article_html.addClass('low-stock');
}
article_html.find('.price').text(amountToUKF(article['price'], false, false)+' UKF');
var category_html = articles_container var category_html = articles_container
.find('#data-category-'+article['category_id']); .find('#data-category-'+article['category_id']);
if (category_html.length == 0) { if (category_html.length == 0) {
@ -642,7 +646,7 @@ $(document).ready(function() {
}); });
$after.after(article_html); $after.after(article_html);
// Pour l'autocomplétion // Pour l'autocomplétion
articlesList.push([article['name'],article['id'],article['category_id'],article['price']]); articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock']]);
} }
function getArticles() { function getArticles() {
@ -670,8 +674,9 @@ $(document).ready(function() {
var articleSelect = $('#article_autocomplete'); var articleSelect = $('#article_autocomplete');
var articleId = $('#article_id'); var articleId = $('#article_id');
var articleNb = $('#article_number'); var articleNb = $('#article_number');
// 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12 var articleStock = $('#article_stock');
var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/; // 8:Backspace|9:Tab|13:Enter|38-40:Arrows|46:DEL|112-117:F1-6|119-123:F8-F12
var normalKeys = /^(8|9|13|37|38|39|40|46|112|113|114|115|116|117|119|120|121|122|123)$/;
var articlesList = []; var articlesList = [];
function deleteNonMatching(array, str) { function deleteNonMatching(array, str) {
@ -727,6 +732,7 @@ $(document).ready(function() {
if (commit) { if (commit) {
articleId.val(articlesMatch[0][1]); articleId.val(articlesMatch[0][1]);
articleSelect.val(articlesMatch[0][0]); articleSelect.val(articlesMatch[0][0]);
articleStock.text('/'+articlesMatch[0][4]);
displayMatchedArticles(articlesList); displayMatchedArticles(articlesList);
return true; return true;
} }
@ -773,10 +779,15 @@ $(document).ready(function() {
return $article.find('.name').text(); return $article.find('.name').text();
} }
function getArticleStock($article) {
return $article.find('.stock').text();
}
// Sélection des articles à la souris/tactile // Sélection des articles à la souris/tactile
articles_container.on('click', '.article', function() { articles_container.on('click', '.article', function() {
articleId.val(getArticleId($(this))); articleId.val(getArticleId($(this)));
articleSelect.val(getArticleName($(this))); articleSelect.val(getArticleName($(this)));
articleStock.text('/'+getArticleStock($(this)));
displayMatchedArticles(articlesList); displayMatchedArticles(articlesList);
goToArticleNb(); goToArticleNb();
}); });
@ -790,6 +801,7 @@ $(document).ready(function() {
addPurchase(articleId.val(), articleNb.val()); addPurchase(articleId.val(), articleNb.val());
articleSelect.val(''); articleSelect.val('');
articleNb.val(''); articleNb.val('');
articleStock.text('');
articleSelect.focus(); articleSelect.focus();
displayMatchedArticles(articlesList); displayMatchedArticles(articlesList);
return false; return false;
@ -809,7 +821,7 @@ $(document).ready(function() {
// Basket // Basket
// ----- // -----
var item_basket_default_html = '<tr><td class="amount"></td><td class="number"></td><td class="name"></td></tr>'; var item_basket_default_html = '<tr><td class="amount"></td><td class="number"></td><td ><span class="lowstock glyphicon glyphicon-alert"></span></td><td class="name"></td></tr>';
var basket_container = $('#basket table'); var basket_container = $('#basket table');
var arrowKeys = /^(37|38|39|40)$/; var arrowKeys = /^(37|38|39|40)$/;
@ -827,17 +839,40 @@ $(document).ready(function() {
} }
function addPurchase(id, nb) { function addPurchase(id, nb) {
var existing = false;
formset_container.find('[data-opeindex]').each(function () {
var opeindex = $(this).attr('data-opeindex');
var article_id = $(this).find('#id_form-'+opeindex+'-article').val();
if (article_id == id) {
existing = true ;
addExistingPurchase(opeindex, nb);
}
});
if (!existing) {
var amount_euro = amountEuroPurchase(id, nb).toFixed(2); var amount_euro = amountEuroPurchase(id, nb).toFixed(2);
var index = addPurchaseToFormset(article_data[1], nb, amount_euro); var index = addPurchaseToFormset(article_data[1], nb, amount_euro);
article_basket_html = $(item_basket_default_html); article_basket_html = $(item_basket_default_html);
article_basket_html article_basket_html
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(nb).end() .find('.number').text('('+nb+'/'+article_data[4]+')').end()
.find('.name').text(article_data[0]).end() .find('.name').text(article_data[0]).end()
.find('.amount').text(amountToUKF(amount_euro, account_data['is_cof'])); .find('.amount').text(amountToUKF(amount_euro, account_data['is_cof']), false);
basket_container.prepend(article_basket_html); basket_container.prepend(article_basket_html);
if (is_low_stock(id, nb))
article_basket_html.find('.lowstock')
.show();
updateBasketRel(); updateBasketRel();
} }
}
function is_low_stock(id, nb) {
var i = 0 ;
while (i<articlesList.length && id != articlesList[i][1]) i++;
article_data = articlesList[i];
stock = article_data[4] ;
return (-5 <= stock - nb && stock - nb <= 5);
}
function addDeposit(amount, is_checkout=1) { function addDeposit(amount, is_checkout=1) {
var deposit_basket_html = $(item_basket_default_html); var deposit_basket_html = $(item_basket_default_html);
@ -848,7 +883,7 @@ $(document).ready(function() {
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(amount+"€").end() .find('.number').text(amount+"€").end()
.find('.name').text(text).end() .find('.name').text(text).end()
.find('.amount').text(amountToUKF(amount, account_data['is_cof'])); .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false));
basket_container.prepend(deposit_basket_html); basket_container.prepend(deposit_basket_html);
updateBasketRel(); updateBasketRel();
} }
@ -861,7 +896,7 @@ $(document).ready(function() {
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(amount+"€").end() .find('.number').text(amount+"€").end()
.find('.name').text('Retrait').end() .find('.name').text('Retrait').end()
.find('.amount').text(amountToUKF(amount, account_data['is_cof'])); .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false));
basket_container.prepend(withdraw_basket_html); basket_container.prepend(withdraw_basket_html);
updateBasketRel(); updateBasketRel();
} }
@ -871,11 +906,25 @@ $(document).ready(function() {
}); });
$(document).on('keydown', function (e) { $(document).on('keydown', function (e) {
if (e.keyCode == 46) { switch(e.which) {
case 46:
// DEL (Suppr) // DEL (Suppr)
basket_container.find('.ui-selected').each(function () { basket_container.find('.ui-selected').each(function () {
deleteFromBasket($(this).data('opeindex')); deleteFromBasket($(this).data('opeindex'));
}); });
break;
case 38:
// Arrow up
basket_container.find('.ui-selected').each(function () {
addExistingPurchase($(this).data('opeindex'), 1);
});
break;
case 40:
// Arrow down
basket_container.find('.ui-selected').each(function () {
addExistingPurchase($(this).data('opeindex'), -1);
});
break;
} }
}); });
@ -903,7 +952,7 @@ $(document).ready(function() {
var amount = $(this).find('#id_form-'+opeindex+'-amount'); var amount = $(this).find('#id_form-'+opeindex+'-amount');
if (!deleted && type == "purchase") if (!deleted && type == "purchase")
amount.val(amountEuroPurchase(article_id, article_nb)); amount.val(amountEuroPurchase(article_id, article_nb));
basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), account_data['is_cof'])); basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), account_data['is_cof'], false));
}); });
} }
@ -922,9 +971,9 @@ $(document).ready(function() {
basketrel_html += '<div>Sur 20€: '+ (20-amount).toFixed(2) +' €</div>'; basketrel_html += '<div>Sur 20€: '+ (20-amount).toFixed(2) +' €</div>';
} else if (account_data['trigramme'] != '' && !isBasketEmpty()) { } else if (account_data['trigramme'] != '' && !isBasketEmpty()) {
var amount = getAmountBasket(); var amount = getAmountBasket();
var amountUKF = amountToUKF(amount, account_data['is_cof']); var amountUKF = amountToUKF(amount, account_data['is_cof'], false);
var newBalance = account_data['balance'] + amount; var newBalance = account_data['balance'] + amount;
var newBalanceUKF = amountToUKF(newBalance, account_data['is_cof']); var newBalanceUKF = amountToUKF(newBalance, account_data['is_cof'], true);
basketrel_html += '<div>Total: '+amountUKF+'</div>'; basketrel_html += '<div>Total: '+amountUKF+'</div>';
basketrel_html += '<div>Nouveau solde: '+newBalanceUKF+'</div>'; basketrel_html += '<div>Nouveau solde: '+newBalanceUKF+'</div>';
if (newBalance < 0) if (newBalance < 0)
@ -939,6 +988,46 @@ $(document).ready(function() {
updateBasketRel(); updateBasketRel();
} }
function addExistingPurchase(opeindex, nb) {
var type = formset_container.find("#id_form-"+opeindex+"-type").val();
var id = formset_container.find("#id_form-"+opeindex+"-article").val();
var nb_before = formset_container.find("#id_form-"+opeindex+"-article_nb").val();
var nb_after = parseInt(nb_before) + parseInt(nb);
var amountEuro_after = amountEuroPurchase(id, nb_after);
var amountUKF_after = amountToUKF(amountEuro_after, account_data['is_cof']);
if (type == 'purchase') {
if (nb_after == 0) {
deleteFromBasket(opeindex);
} else if (nb_after > 0 && nb_after <= 25) {
if (nb_before > 0) {
var article_html = basket_container.find('[data-opeindex='+opeindex+']');
article_html.find('.amount').text(amountUKF_after).end()
.find('.number').text('('+nb_after+'/'+article_data[4]+')').end() ;
} else {
article_html = $(item_basket_default_html);
article_html
.attr('data-opeindex', opeindex)
.find('.number').text('('+nb_after+'/'+article_data[4]+')').end()
.find('.name').text(article_data[0]).end()
.find('.amount').text(amountUKF_after);
basket_container.prepend(article_basket_html);
}
if (is_low_stock(id, nb_after))
article_html.find('.lowstock')
.show();
else
article_html.find('.lowstock')
.hide();
updateExistingFormset(opeindex, nb_after, amountEuro_after);
updateBasketRel();
}
}
}
function resetBasket() { function resetBasket() {
basket_container.find('tr').remove(); basket_container.find('tr').remove();
mngmt_total_forms = 1; mngmt_total_forms = 1;
@ -948,6 +1037,7 @@ $(document).ready(function() {
articleId.val(0); articleId.val(0);
articleSelect.val(''); articleSelect.val('');
articleNb.val(''); articleNb.val('');
articleStock.text('');
displayMatchedArticles(articlesList); displayMatchedArticles(articlesList);
} }
@ -1072,7 +1162,14 @@ $(document).ready(function() {
} }
function deleteFromFormset(opeindex) { function deleteFromFormset(opeindex) {
formset_container.find('#id_form-'+opeindex+'-DELETE').prop('checked', true); updateExistingFormset(opeindex, 0, '0.00');
}
function updateExistingFormset(opeindex, nb, amount) {
formset_container
.find('#id_form-'+opeindex+'-amount').val((parseFloat(amount)).toFixed(2)).end()
.find('#id_form-'+opeindex+'-article_nb').val(nb).end()
.find('#id_form-'+opeindex+'-DELETE').prop('checked', !nb);
} }
// ----- // -----
@ -1251,6 +1348,8 @@ $(document).ready(function() {
} }
for (var i=0; i<data['articles'].length; i++) { for (var i=0; i<data['articles'].length; i++) {
article = data['articles'][i]; article = data['articles'][i];
articles_container.find('#data-article-'+article['id'])
.addClass('low-stock');
articles_container.find('#data-article-'+article['id']+' .stock') articles_container.find('#data-article-'+article['id']+' .stock')
.text(article['stock']); .text(article['stock']);
} }

View file

@ -0,0 +1,5 @@
from django.template.defaulttags import register
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)

5
kfet/tests.py Normal file
View file

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
# Écrire les tests ici

View file

@ -68,6 +68,30 @@ urlpatterns = [
(views.AccountNegativeList.as_view()), (views.AccountNegativeList.as_view()),
name='kfet.account.negative'), name='kfet.account.negative'),
# Account - Statistics
url('^accounts/(?P<trigramme>.{3})/stat/last/$',
views.AccountStatLastAll.as_view(),
name = 'kfet.account.stat.last'),
url('^accounts/(?P<trigramme>.{3})/stat/last/month/$',
views.AccountStatLastMonth.as_view(),
name = 'kfet.account.stat.last.month'),
url('^accounts/(?P<trigramme>.{3})/stat/last/week/$',
views.AccountStatLastWeek.as_view(),
name = 'kfet.account.stat.last.week'),
url('^accounts/(?P<trigramme>.{3})/stat/last/day/$',
views.AccountStatLastDay.as_view(),
name = 'kfet.account.stat.last.day'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/$',
views.AccountStatBalanceAll.as_view(),
name = 'kfet.account.stat.balance'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/d/(?P<nb_date>\d*)/$',
views.AccountStatBalance.as_view(),
name = 'kfet.account.stat.balance.days'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/anytime/$',
views.AccountStatBalance.as_view(),
name = 'kfet.account.stat.balance.anytime'),
# ----- # -----
# Checkout urls # Checkout urls
# ----- # -----
@ -126,6 +150,19 @@ urlpatterns = [
url('^articles/(?P<pk>\d+)/edit$', url('^articles/(?P<pk>\d+)/edit$',
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/last/$',
views.ArticleStatLastAll.as_view(),
name = 'kfet.article.stat.last'),
url('^articles/(?P<pk>\d+)/stat/last/month/$',
views.ArticleStatLastMonth.as_view(),
name = 'kfet.article.stat.last.month'),
url('^articles/(?P<pk>\d+)/stat/last/week/$',
views.ArticleStatLastWeek.as_view(),
name = 'kfet.article.stat.last.week'),
url('^articles/(?P<pk>\d+)/stat/last/day/$',
views.ArticleStatLastDay.as_view(),
name = 'kfet.article.stat.last.day'),
# ----- # -----
# K-Psul urls # K-Psul urls

View file

@ -1,44 +1,61 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied
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.list import BaseListView, MultipleObjectTemplateResponseMixin
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, reverse_lazy
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission, Group from django.contrib.auth.models import User, Permission, Group
from django.http import HttpResponse, JsonResponse, Http404 from django.http import JsonResponse, Http404
from django.forms import modelformset_factory, formset_factory from django.forms import formset_factory
from django.db import IntegrityError, transaction from django.db import transaction
from django.db.models import F, Sum, Prefetch, Count, Func from django.db.models import F, Sum, Prefetch, Count
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) InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
from kfet.forms import * TransferGroup, Transfer)
from kfet.forms import (
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm,
GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
KPsulOperationFormSet, AddcostForm, FilterHistoryForm, SettingsForm,
TransferFormSet, InventoryArticleForm, OrderArticleForm,
OrderArticleToInventoryForm
)
from collections import defaultdict from collections import defaultdict
from kfet import consumers from kfet import consumers
from datetime import timedelta from datetime import timedelta
from decimal import Decimal
import django_cas_ng import django_cas_ng
import hashlib
import heapq import heapq
import statistics import statistics
from .statistic import daynames, monthnames, weeknames, \
lastdays, lastweeks, lastmonths, \
this_morning, this_monday_morning, this_first_month_day, \
tot_ventes
@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):
@ -345,6 +362,7 @@ def account_read(request, trigramme):
# Account - Update # Account - Update
@login_required @login_required
def account_update(request, trigramme): def account_update(request, trigramme):
account = get_object_or_404(Account, trigramme=trigramme) account = get_object_or_404(Account, trigramme=trigramme)
@ -361,7 +379,8 @@ def account_update(request, trigramme):
cof_form = CofRestrictForm(instance=account.cofprofile) cof_form = CofRestrictForm(instance=account.cofprofile)
pwd_form = AccountPwdForm() pwd_form = AccountPwdForm()
if account.balance < 0 and not hasattr(account, 'negative'): if account.balance < 0 and not hasattr(account, 'negative'):
AccountNegative.objects.create(account=account, start=timezone.now()) AccountNegative.objects.create(account=account,
start=timezone.now())
account.refresh_from_db() account.refresh_from_db()
if hasattr(account, 'negative'): if hasattr(account, 'negative'):
negative_form = AccountNegativeForm(instance=account.negative) negative_form = AccountNegativeForm(instance=account.negative)
@ -382,12 +401,15 @@ def account_update(request, trigramme):
if request.user.has_perm('kfet.is_team'): if request.user.has_perm('kfet.is_team'):
account_form = AccountForm(request.POST, instance=account) account_form = AccountForm(request.POST, instance=account)
cof_form = CofRestrictForm(request.POST, instance=account.cofprofile) cof_form = CofRestrictForm(request.POST,
user_form = UserRestrictTeamForm(request.POST, instance=account.user) instance=account.cofprofile)
user_form = UserRestrictTeamForm(request.POST,
instance=account.user)
group_form = UserGroupForm(request.POST, instance=account.user) group_form = UserGroupForm(request.POST, instance=account.user)
pwd_form = AccountPwdForm(request.POST) pwd_form = AccountPwdForm(request.POST)
if hasattr(account, 'negative'): if hasattr(account, 'negative'):
negative_form = AccountNegativeForm(request.POST, instance=account.negative) negative_form = AccountNegativeForm(request.POST,
instance=account.negative)
if (request.user.has_perm('kfet.change_account') if (request.user.has_perm('kfet.change_account')
and account_form.is_valid() and cof_form.is_valid() and account_form.is_valid() and cof_form.is_valid()
@ -405,9 +427,8 @@ def account_update(request, trigramme):
if (request.user.has_perm('kfet.change_account_password') if (request.user.has_perm('kfet.change_account_password')
and pwd_form.is_valid()): and pwd_form.is_valid()):
pwd = pwd_form.cleaned_data['pwd1'] pwd = pwd_form.cleaned_data['pwd1']
pwd_sha256 = hashlib.sha256(pwd.encode('utf-8')).hexdigest() account.change_pwd(pwd)
Account.objects.filter(pk=account.pk).update( account.save()
password = pwd_sha256)
messages.success(request, 'Mot de passe mis à jour') messages.success(request, 'Mot de passe mis à jour')
# Checking perm to manage perms # Checking perm to manage perms
@ -423,38 +444,55 @@ def account_update(request, trigramme):
if (hasattr(account, 'negative') if (hasattr(account, 'negative')
and request.user.has_perm('kfet.change_accountnegative') and request.user.has_perm('kfet.change_accountnegative')
and negative_form.is_valid()): and negative_form.is_valid()):
balance_offset_new = negative_form.cleaned_data['balance_offset'] balance_offset_new = \
negative_form.cleaned_data['balance_offset']
if not balance_offset_new: if not balance_offset_new:
balance_offset_new = 0 balance_offset_new = 0
balance_offset_diff = balance_offset_new - balance_offset_old balance_offset_diff = (balance_offset_new
- balance_offset_old)
Account.objects.filter(pk=account.pk).update( Account.objects.filter(pk=account.pk).update(
balance=F('balance') + balance_offset_diff) balance=F('balance') + balance_offset_diff)
negative_form.save() negative_form.save()
if not balance_offset_new and Account.objects.get(pk=account.pk).balance >= 0: if Account.objects.get(pk=account.pk).balance >= 0 \
and not balance_offset_new:
AccountNegative.objects.get(account=account).delete() AccountNegative.objects.get(account=account).delete()
success = True success = True
messages.success(request, messages.success(
'Informations du compte %s mises à jour' % account.trigramme) request,
'Informations du compte %s mises à jour'
% account.trigramme)
# Modification de ses propres informations
if request.user == account.user: if request.user == account.user:
missing_perm = False missing_perm = False
account.refresh_from_db() account.refresh_from_db()
user_form = UserRestrictForm(request.POST, instance=account.user) user_form = UserRestrictForm(request.POST, instance=account.user)
account_form = AccountRestrictForm(request.POST, instance=account) account_form = AccountRestrictForm(request.POST, instance=account)
pwd_form = AccountPwdForm(request.POST)
if user_form.is_valid() and account_form.is_valid(): if user_form.is_valid() and account_form.is_valid():
user_form.save() user_form.save()
account_form.save() account_form.save()
success = True success = True
messages.success(request, 'Vos informations ont été mises à jour') messages.success(request,
'Vos informations ont été mises à jour')
if request.user.has_perm('kfet.is_team') \
and pwd_form.is_valid():
pwd = pwd_form.cleaned_data['pwd1']
account.change_pwd(pwd)
account.save()
messages.success(
request, 'Votre mot de passe a été mis à jour')
if missing_perm: if missing_perm:
messages.error(request, 'Permission refusée') messages.error(request, 'Permission refusée')
if success: if success:
return redirect('kfet.account.read', account.trigramme) return redirect('kfet.account.read', account.trigramme)
else: else:
messages.error(request, 'Informations non mises à jour. Corrigez les erreurs') messages.error(
request, 'Informations non mises à jour. Corrigez les erreurs')
return render(request, "kfet/account_update.html", { return render(request, "kfet/account_update.html", {
'account': account, 'account': account,
@ -795,6 +833,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
# ----- # -----
@ -1944,3 +1984,591 @@ 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
class JSONDetailView(JSONResponseMixin,
BaseDetailView):
"""
Returns a DetailView that renders a JSON
"""
def render_to_response(self, context):
return self.render_to_json_response(context)
class HybridDetailView(JSONResponseMixin,
SingleObjectTemplateResponseMixin,
BaseDetailView):
"""
Returns a DetailView as an html page except if a JSON file is requested
by the GET method in which 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 an html page except if a JSON file is requested
by the GET method in which 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(JSONDetailView):
"""
Summarize all the stats of an object
Handles JSONResponse
"""
context_object_name = ''
id_prefix = ''
# 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 url_kwargs(self, **kwargs):
return [{}] * self.nb_stat
def get_context_data(self, **kwargs):
# On n'hérite pas
object_id = self.object.id
url_kwargs = self.url_kwargs()
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(self.stat_urls[i],
kwargs=dict(
self.get_object_url_kwargs(),
**url_kwargs[i]
),
),
}
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
# -----------------------
# Evolution Balance perso
# -----------------------
ID_PREFIX_ACC_BALANCE = "balance_acc"
# Un résumé de toutes les vues ArticleStatBalance
# REND DU JSON
class AccountStatBalanceAll(ObjectResumeStat):
model = Account
context_object_name = 'account'
trigramme_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_BALANCE
nb_stat = 5
nb_default = 0
stat_labels = ["Tout le temps", "1 an", "6 mois", "3 mois", "30 jours"]
stat_urls = ['kfet.account.stat.balance.anytime'] \
+ ['kfet.account.stat.balance.days'] * 4
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def get_object_url_kwargs(self, **kwargs):
return {'trigramme': self.object.trigramme}
def url_kwargs(self, **kwargs):
context_list = (super(AccountStatBalanceAll, self)
.url_kwargs(**kwargs))
context_list[1] = {'nb_date': 365}
context_list[2] = {'nb_date': 183}
context_list[3] = {'nb_date': 90}
context_list[4] = {'nb_date': 30}
return context_list
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
class AccountStatBalance(JSONDetailView):
"""
Returns a JSON containing the evolution a the personnal
balance of a trigramme between timezone.now() and `nb_days`
ago (specified to the view as an argument)
takes into account the Operations and the Transfers
does not takes into account the balance offset
"""
model = Account
trigramme_url_kwarg = 'trigramme'
nb_date_url_kwargs = 'nb_date'
context_object_name = 'account'
id_prefix = ""
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
nb_date = self.kwargs.get(self.nb_date_url_kwargs, None)
end_date = this_morning()
if nb_date is None:
begin_date = timezone.datetime(year=1980, month=1, day=1)
anytime = True
else:
begin_date = this_morning() \
- timezone.timedelta(days=int(nb_date))
anytime = False
# 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=begin_date)
.filter(at__lte=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=begin_date)
.filter(group__at__lte=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=begin_date)
.filter(group__at__lte=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': 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 anytime:
actions += [
# Date de début :
{
'at': 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()
nb_days = self.kwargs.get(self.nb_date_url_kwargs, None)
if nb_days is None:
nb_days_string = 'anytime'
else:
nb_days_string = str(int(nb_days))
context['charts'] = [ { "color": "rgb(255, 99, 132)",
"label": "Balance",
"values": changes } ]
context['is_time_chart'] = True
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)
# ------------------------
# Consommation personnelle
# ------------------------
ID_PREFIX_ACC_LAST = "last_acc"
ID_PREFIX_ACC_LAST_DAYS = "last_days_acc"
ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc"
ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
# Un résumé de toutes les vues ArticleStatLast
# NE REND PAS DE JSON
class AccountStatLastAll(ObjectResumeStat):
model = Account
context_object_name = 'account'
trigramme_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST
nb_stat = 3
nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
stat_urls = ['kfet.account.stat.last.month',
'kfet.account.stat.last.week',
'kfet.account.stat.last.day']
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
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(JSONDetailView):
"""
Returns a JSON containing the evolution a the personnal
consommation of a trigramme at the diffent dates specified
"""
model = Account
trigramme_url_kwarg = 'trigramme'
context_object_name = 'account'
end_date = timezone.now()
id_prefix = ""
# 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):
return {}
# 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):
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['charts'] = [ { "color": "rgb(255, 99, 132)",
"label": "NB items achetés",
"values": nb_ventes } ]
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
class AccountStatLastDay(AccountStatLast):
end_date = this_morning()
id_prefix = ID_PREFIX_ACC_LAST_DAYS
def get_dates(self, **kwargs):
return lastdays(7)
def get_labels(self, **kwargs):
days = lastdays(7)
return daynames(days)
# Rend les achats de ce compte des 7 dernières semaines
# La semaine en cours n'est pas comprise
class AccountStatLastWeek(AccountStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ACC_LAST_WEEKS
def get_dates(self, **kwargs):
return lastweeks(7)
def get_labels(self, **kwargs):
weeks = lastweeks(7)
return weeknames(weeks)
# Rend les achats de ce compte des 7 derniers mois
# Le mois en cours n'est pas compris
class AccountStatLastMonth(AccountStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ACC_LAST_MONTHS
def get_dates(self, **kwargs):
return lastmonths(7)
def get_labels(self, **kwargs):
months = lastmonths(7)
return monthnames(months)
# ------------------------
# Article Satistiques Last
# ------------------------
ID_PREFIX_ART_LAST = "last_art"
ID_PREFIX_ART_LAST_DAYS = "last_days_art"
ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
# Un résumé de toutes les vues ArticleStatLast
# NE REND PAS DE JSON
class ArticleStatLastAll(ObjectResumeStat):
model = Article
context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST
nb_stat = 3
nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
stat_urls = ['kfet.article.stat.last.month',
'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)
class ArticleStatLast(JSONDetailView):
"""
Returns a JSON containing the consommation
of an article at the diffent dates precised
"""
model = Article
context_object_name = 'article'
end_date = timezone.now()
id_prefix = ""
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):
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['charts'] = [ { "color": "rgb(255, 99, 132)",
"label": "Toutes consommations",
"values": nb_ventes },
{ "color": "rgb(54, 162, 235)",
"label": "LIQ",
"values": nb_liq },
{ "color": "rgb(255, 205, 86)",
"label": "Comptes K-Fêt",
"values": nb_accounts } ]
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
class ArticleStatLastDay(ArticleStatLast):
end_date = this_morning()
id_prefix = ID_PREFIX_ART_LAST_DAYS
def get_dates(self, **kwargs):
return lastdays(7)
def get_labels(self, **kwargs):
days = lastdays(7)
return daynames(days)
# Rend les ventes de 7 dernières semaines
# La semaine en cours n'est pas comprise
class ArticleStatLastWeek(ArticleStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ART_LAST_WEEKS
def get_dates(self, **kwargs):
return lastweeks(7)
def get_labels(self, **kwargs):
weeks = lastweeks(7)
return weeknames(weeks)
# Rend les ventes des 7 derniers mois
# Le mois en cours n'est pas compris
class ArticleStatLastMonth(ArticleStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ART_LAST_MONTHS
def get_dates(self, **kwargs):
return lastmonths(7)
def get_labels(self, **kwargs):
months = lastmonths(7)
return monthnames(months)