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

This commit is contained in:
Qwann 2017-03-05 19:44:08 +01:00
commit b0b2210e93
15 changed files with 511 additions and 557 deletions

View file

@ -1,21 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.forms import modelformset_factory, inlineformset_factory from django.forms import modelformset_factory
from django.forms.models import BaseInlineFormSet
from django.utils import timezone from django.utils import timezone
from kfet.models import (Account, Checkout, Article, OperationGroup, Operation, from kfet.models import (
Account, Checkout, Article, OperationGroup, Operation,
CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer, CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer,
TransferGroup, Supplier, Inventory, InventoryArticle) TransferGroup, Supplier)
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
# ----- # -----
@ -131,7 +127,16 @@ class UserRestrictTeamForm(UserForm):
class UserGroupForm(forms.ModelForm): class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField( groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt')) Group.objects.filter(name__icontains='K-Fêt'),
required=False)
def clean_groups(self):
groups = self.cleaned_data.get('groups')
# Si aucun groupe, on le dénomme
if not groups:
groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return groups
class Meta: class Meta:
model = User model = User
fields = ['groups'] fields = ['groups']

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
@ -561,7 +568,7 @@ class Operation(models.Model):
templates = { templates = {
self.PURCHASE: "{nb} {article.name} ({amount}€)", self.PURCHASE: "{nb} {article.name} ({amount}€)",
self.DEPOSIT: "charge ({amount})", self.DEPOSIT: "charge ({amount})",
self.WITHDRAW: "retrait ({amount})", self.WITHDRAW: "retrait ({amount})",
self.INITIAL: "initial ({amount})", self.INITIAL: "initial ({amount})",
} }
return templates[self.type].format(nb=self.article_nb, return templates[self.type].format(nb=self.article_nb,

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;

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

@ -1,18 +1,197 @@
var STAT = {}; (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;
jQuery(document).ready(function() { function dictToArray (dict, start) {
// FONCTIONS // converts the dicts returned by JSONResponse to Arrays
// Permet de raffraichir un champ, étant donné : // necessary because for..in does not guarantee the order
// thing_url : l'url contenant le contenu if (start === undefined) start = 0;
// thing_div : le div où le mettre var array = new Array();
// empty_... : le truc à dire si on a un contenu vide for (var k in dict) {
STAT.get_thing = function(thing_url, thing_div, empty_thing_message) { array[k] = dict[k];
$.get(thing_url, function(data) { }
if(jQuery.trim(data).length==0) { array.splice(0, start);
thing_div.html(empty_thing_message); return array;
} else { }
thing_div.html(data);
} 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);

View file

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.utils import timezone 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 # donne le nom des jours d'une liste de dates
# dans un dico ordonné # dans un dico ordonné
@ -28,6 +30,7 @@ def monthnames(dates):
names[i] = dates[i].strftime("%B") names[i] = dates[i].strftime("%B")
return names return names
# rend les dates des nb derniers jours # rend les dates des nb derniers jours
# dans l'ordre chronologique # dans l'ordre chronologique
# aujourd'hui compris # aujourd'hui compris
@ -55,11 +58,12 @@ def lastmonths(nb):
this_year = first_month_day.year this_year = first_month_day.year
this_month = first_month_day.month this_month = first_month_day.month
for i in range(1, nb+1): for i in range(1, nb+1):
month = ((this_month - 1 - (nb - i)) % 12) + 1 month = ((this_month - 1 - (nb - i)) % 12) + 1
year = this_year + (nb - i) // 12 year = this_year + (nb - i) // 12
first_days[i] = timezone.datetime(year=year, first_days[i] = timezone.datetime(year=year,
month=month, month=month,
day=1) day=1,
hour=KFET_WAKES_UP_AT)
return first_days return first_days
@ -67,7 +71,8 @@ def this_first_month_day():
now = timezone.now() now = timezone.now()
first_day = timezone.datetime(year=now.year, first_day = timezone.datetime(year=now.year,
month=now.month, month=now.month,
day=1) day=1,
hour=KFET_WAKES_UP_AT)
return first_day return first_day
@ -76,7 +81,8 @@ def this_monday_morning():
monday = now - timezone.timedelta(days=now.isoweekday()-1) monday = now - timezone.timedelta(days=now.isoweekday()-1)
monday_morning = timezone.datetime(year=monday.year, monday_morning = timezone.datetime(year=monday.year,
month=monday.month, month=monday.month,
day=monday.day) day=monday.day,
hour=KFET_WAKES_UP_AT)
return monday_morning return monday_morning
@ -84,14 +90,13 @@ def this_morning():
now = timezone.now() now = timezone.now()
morning = timezone.datetime(year=now.year, morning = timezone.datetime(year=now.year,
month=now.month, month=now.month,
day=now.day) day=now.day,
hour=KFET_WAKES_UP_AT)
return morning return morning
# Étant donné un queryset d'operations # Étant donné un queryset d'operations
# rend la somme des article_nb # rend la somme des article_nb
def tot_ventes(queryset): def tot_ventes(queryset):
res = 0 res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
for op in queryset: return res and res or 0
res += op.article_nb
return res

View file

@ -14,14 +14,12 @@
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {
var stat_last = $("#stat_last"); var stat_last = new StatsGroup("{% url 'kfet.account.stat.last' trigramme=account.trigramme %}",
var stat_balance = $("#stat_balance"); $("#stat_last"));
var stat_last_url = "{% url 'kfet.account.stat.last' trigramme=account.trigramme %}"; var stat_balance = new StatsGroup("{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}",
var stat_balance_url = "{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}"; $("#stat_balance"));
STAT.get_thing(stat_last_url, stat_last, "Stat non trouvées :("); });
STAT.get_thing(stat_balance_url, stat_balance, "Stat non trouvées :(");
});
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,91 +0,0 @@
<!doctype html>
{% load dictionary_extras %}
<body>
<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() {
var ctx1 = $({{ chart_id }});
var myChart = new Chart(ctx1, {
type: 'line',
data: {
datasets: [{
label: 'Balance',
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
data: [
{% 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: true,
steppedLine: true,
lineTension: 0,
}]
},
options: {
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
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'
}
}]
}
}
});
});
</script>
</body>

View file

@ -1,52 +0,0 @@
<!doctype html>
<body>
<canvas id="{{ chart_id }}" height="100" ></canvas>
<script>
jQuery(document).ready(function() {
var ctx1 = $({{ chart_id }});
var myChart = new Chart(ctx1, {
type: 'line',
data: {
labels: [
{% for k,label in labels.items %}
{% if forloop.last %}
"{{ label }}"
{% else %}
"{{ label }}",
{% endif %}
{% endfor %}
],
datasets: [{
label: 'Nb items achetés',
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgb(255, 99, 132)',
data: [
{% for k,nb in nb_ventes.items %}
{% if forloop.last %}
"{{ nb }}"
{% else %}
"{{ nb }}",
{% endif %}
{% endfor %}
],
fill: false,
lineTension: 0,
}]
},
options: {
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'nearest',
intersect: false,
},
}
});
});
</script>
</body>

View file

@ -1,82 +0,0 @@
<!doctype html>
<body>
<canvas id="{{ chart_id }}" height="100" ></canvas>
<script>
jQuery(document).ready(function() {
var ctx1 = $({{ chart_id }});
var myChart = new Chart(ctx1, {
type: 'line',
data: {
labels: [
{% for k,label in labels.items %}
{% if forloop.last %}
"{{ label }}"
{% else %}
"{{ label }}",
{% endif %}
{% endfor %}
],
datasets: [{
label: 'Toutes consommations',
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgb(255, 99, 132)',
data: [
{% for k,nb in nb_ventes.items %}
{% if forloop.last %}
"{{ nb }}"
{% else %}
"{{ nb }}",
{% endif %}
{% endfor %}
],
fill: false,
lineTension: 0,
} , {
label: 'LIQ',
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgb(54, 162, 235)',
data: [
{% for k,nb in nb_liq.items %}
{% if forloop.last %}
"{{ nb }}"
{% else %}
"{{ nb }}",
{% endif %}
{% endfor %}
],
fill: false,
lineTension: 0,
} , {
label: 'Comptes K-Fêt',
borderColor: 'rgb(255, 205, 86)',
backgroundColor: 'rgb(255, 205, 86)',
data: [
{% for k,nb in nb_accounts.items %}
{% if forloop.last %}
"{{ nb }}"
{% else %}
"{{ nb }}",
{% endif %}
{% endfor %}
],
fill: false,
lineTension: 0,
}]
},
options: {
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'nearest',
intersect: false,
},
}
});
});
</script>
</body>

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,16 +839,39 @@ $(document).ready(function() {
} }
function addPurchase(id, nb) { function addPurchase(id, nb) {
var amount_euro = amountEuroPurchase(id, nb).toFixed(2); var existing = false;
var index = addPurchaseToFormset(article_data[1], nb, amount_euro); formset_container.find('[data-opeindex]').each(function () {
article_basket_html = $(item_basket_default_html); var opeindex = $(this).attr('data-opeindex');
article_basket_html var article_id = $(this).find('#id_form-'+opeindex+'-article').val();
.attr('data-opeindex', index) if (article_id == id) {
.find('.number').text(nb).end() existing = true ;
.find('.name').text(article_data[0]).end() addExistingPurchase(opeindex, nb);
.find('.amount').text(amountToUKF(amount_euro, account_data['is_cof'])); }
basket_container.prepend(article_basket_html); });
updateBasketRel(); if (!existing) {
var amount_euro = amountEuroPurchase(id, nb).toFixed(2);
var index = addPurchaseToFormset(article_data[1], nb, amount_euro);
article_basket_html = $(item_basket_default_html);
article_basket_html
.attr('data-opeindex', index)
.find('.number').text('('+nb+'/'+article_data[4]+')').end()
.find('.name').text(article_data[0]).end()
.find('.amount').text(amountToUKF(amount_euro, account_data['is_cof']), false);
basket_container.prepend(article_basket_html);
if (is_low_stock(id, nb))
article_basket_html.find('.lowstock')
.show();
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) {
@ -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) {
// DEL (Suppr) case 46:
basket_container.find('.ui-selected').each(function () { // DEL (Suppr)
deleteFromBasket($(this).data('opeindex')); basket_container.find('.ui-selected').each(function () {
}); 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);
} }
@ -959,7 +1049,7 @@ $(document).ready(function() {
var title = is_checkout ? 'Montant de la charge' : "Montant de l'édition"; var title = is_checkout ? 'Montant de la charge' : "Montant de l'édition";
$.confirm({ $.confirm({
title: title, title: title,
content: '<input type="number" step="0.01" min="0.01" on autofocus placeholder="€">', content: '<input type="number" lang="en" step="0.01" min="0.01" on autofocus placeholder="€">',
backgroundDismiss: true, backgroundDismiss: true,
animation:'top', animation:'top',
closeAnimation:'bottom', closeAnimation:'bottom',
@ -986,7 +1076,7 @@ $(document).ready(function() {
function askWithdraw() { function askWithdraw() {
$.confirm({ $.confirm({
title: 'Montant du retrait', title: 'Montant du retrait',
content: '<input type="number" step="0.01" min="0.01" on autofocus placeholder="€">', content: '<input type="number" lang="en" step="0.01" min="0.01" on autofocus placeholder="€">',
backgroundDismiss: true, backgroundDismiss: true,
animation:'top', animation:'top',
closeAnimation:'bottom', closeAnimation:'bottom',
@ -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);
} }
// ----- // -----
@ -1164,7 +1261,7 @@ $(document).ready(function() {
function askAddcost(errors = '') { function askAddcost(errors = '') {
$.confirm({ $.confirm({
title: 'Majoration', title: 'Majoration',
content: errors + '<input type="text" placeholder="Trigramme" autocomplete="off" name="trigramme" spellcheck="false" style="text-transform:uppercase" autofocus><input type="number" step="0.01" min="0.01" placeholder="€" name="amount">', content: errors + '<input type="text" placeholder="Trigramme" autocomplete="off" name="trigramme" spellcheck="false" style="text-transform:uppercase" autofocus><input type="number" lang="en" step="0.01" min="0.01" placeholder="€" name="amount">',
backgroundDismiss: true, backgroundDismiss: true,
animation:'top', animation:'top',
closeAnimation:'bottom', closeAnimation:'bottom',
@ -1251,7 +1348,9 @@ $(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']+'] .stock') articles_container.find('#data-article-'+article['id'])
.addClass('low-stock');
articles_container.find('#data-article-'+article['id']+' .stock')
.text(article['stock']); .text(article['stock']);
} }
if (data['addcost']) { if (data['addcost']) {

View file

@ -1,35 +0,0 @@
<!doctype html>
{% load staticfiles %}
{% load dictionary_extras %}
<body>
<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 {{ 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() {
// VARIABLES
{{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
STAT.get_thing({{id_prefix}}_default_url, {{id_prefix}}_content_id, "Ouppss ?");
{% for k,stat in stats.items %}
$("#{{stat|get_item:'btn'}}").on('click', function() {
STAT.get_thing("{{stat|get_item:'url'}}", {{id_prefix}}_content_id, "Ouuups ?");
{{id_prefix}}_btns.removeClass("focus");
$("#{{stat|get_item:'btn'}}").addClass("focus");
});
{% endfor %}
});
</script>
</body>

View file

@ -41,5 +41,4 @@ def highlight_clipper(clipper, q):
@register.filter() @register.filter()
def ukf(balance, is_cof): def ukf(balance, is_cof):
grant = is_cof and (1 + Settings.SUBVENTION_COF() / 100) or 1 grant = is_cof and (1 + Settings.SUBVENTION_COF() / 100) or 1
# float nécessaire car sinon problème avec le round de future.builtins return floor(balance * 10 * grant)
return floor(float(balance) * 10 * grant)

View file

@ -2,143 +2,4 @@
from django.test import TestCase from django.test import TestCase
from kfet.models import Account # Écrire les tests ici
from gestioncof.models import CofProfile, User
names = ['Abelardus',
'Abrahamus',
'Acacius',
'Accius',
'Achaicus',
'Achill',
'Achilles',
'Achilleus',
'Acrisius',
'Actaeon',
'Acteon',
'Adalricus',
'Adelfonsus',
'Adelphus',
'Adeodatus',
'Adolfus',
'Adolphus',
'Adrastus',
'Adrianus',
'Ægidius',
'Ælia',
'Ælianus',
'Æmilianus',
'Æmilius',
'Aeneas',
'Æolus',
'Æschylus',
'Æson',
'Æsop',
'Æther',
'Ætius',
'Agapetus',
'Agapitus',
'Agapius',
'Agathangelus',
'Aigidius',
'Aiolus',
'Ajax',
'Alair',
'Alaricus',
'Albanus',
'Alberic',
'Albericus',
'Albertus',
'Albinus',
'Albus',
'Alcaeus',
'Alcander',
'Alcimus',
'Alcinder',
'Alerio',
'Alexandrus',
'Alexis',
'Alexius',
'Alexus',
'Alfonsus',
'Alfredus',
'Almericus',
'Aloisius',
'Aloysius',
'Alphaeus',
'Alpheaus',
'Alpheus',
'Alphoeus',
'Alphonsus',
'Alphonzus',
'Alvinius',
'Alvredus',
'Amadeus',
'Amaliricus',
'Amandus',
'Amantius',
'Amarandus',
'Amaranthus',
'Amatus',
'Ambrosianus',
'Ambrosius',
'Amedeus',
'Americus',
'Amlethus',
'Amletus',
'Amor',
'Ampelius',
'Amphion',
'Anacletus',
'Anastasius',
'Anastatius',
'Anastius',
'Anatolius',
'Androcles',
'Andronicus',
'Anencletus',
'Angelicus',
'Angelus',
'Anicetus',
'Antigonus',
'Antipater',
'Antoninus',
'Antonius',
'Aphrodisius',
'Apollinaris']
# Ne pas supprimer, peut être utile pour regénérer
# d'autres fixtures à l'avenir
class TestFoo(TestCase):
fixtures = ['users', 'sites', 'gestion', 'articles']
def test_foo(self):
pass
# for name in names:
# user = User(username=name,
# last_name='Romain',
# first_name=name)
# user.email = '{}.{}@ens.fr'.format(user.first_name, user.last_name)
# user.save()
# galois_trigrammes = map('{:03d}'.format, range(40))
# galois = CofProfile.objects.filter(user__last_name='Gaulois')
# romains_trigrammes = map(lambda x: str(100+x), range(40))
# romains = CofProfile.objects.filter(user__last_name='Romain')
# for t, c in zip(galois_trigrammes, galois):
# Account.objects.create(cofprofile=c, trigramme=t)
# for t, c in zip(romains_trigrammes, romains):
# Account.objects.create(cofprofile=c, trigramme=t)
# class TestBar(TestCase):
# fixtures = ['users', 'sites', 'gestion', 'articles', 'groups', 'accounts']
# def test_foo(self):
# pass

View file

@ -1,37 +1,49 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
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, TemplateView from django.views.generic import ListView, DetailView, TemplateView
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin 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 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, OperationGroup, Transfer) 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, \ from .statistic import daynames, monthnames, weeknames, \
@ -54,6 +66,7 @@ class Home(TemplateView):
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(TemplateView, self).dispatch(*args, **kwargs) return super(TemplateView, self).dispatch(*args, **kwargs)
@teamkfet_required @teamkfet_required
def login_genericteam(request): def login_genericteam(request):
# Check si besoin de déconnecter l'utilisateur de CAS # Check si besoin de déconnecter l'utilisateur de CAS
@ -359,6 +372,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)
@ -369,39 +383,43 @@ def account_update(request, trigramme):
raise PermissionDenied raise PermissionDenied
if request.user.has_perm('kfet.is_team'): if request.user.has_perm('kfet.is_team'):
user_form = UserRestrictTeamForm(instance=account.user) user_form = UserRestrictTeamForm(instance=account.user)
group_form = UserGroupForm(instance=account.user) group_form = UserGroupForm(instance=account.user)
account_form = AccountForm(instance=account) account_form = AccountForm(instance=account)
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)
else: else:
negative_form = None negative_form = None
else: else:
user_form = UserRestrictForm(instance=account.user) user_form = UserRestrictForm(instance=account.user)
account_form = AccountRestrictForm(instance=account) account_form = AccountRestrictForm(instance=account)
cof_form = None cof_form = None
group_form = None group_form = None
negative_form = None negative_form = None
pwd_form = None pwd_form = None
if request.method == "POST": if request.method == "POST":
# Update attempt # Update attempt
success = False success = False
missing_perm = True missing_perm = True
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)
group_form = UserGroupForm(request.POST, instance=account.user) user_form = UserRestrictTeamForm(request.POST,
pwd_form = AccountPwdForm(request.POST) instance=account.user)
group_form = UserGroupForm(request.POST, instance=account.user)
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()
@ -413,15 +431,14 @@ def account_update(request, trigramme):
put_cleaned_data_in_dict(data, cof_form) put_cleaned_data_in_dict(data, cof_form)
# Updating # Updating
account_form.save(data = data) account_form.save(data=data)
# Checking perm to update password # Checking perm to update password
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
@ -435,49 +452,66 @@ def account_update(request, trigramme):
if account.negative.balance_offset: if account.negative.balance_offset:
balance_offset_old = account.negative.balance_offset balance_offset_old = account.negative.balance_offset
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,
'account_form' : account_form, 'account_form': account_form,
'cof_form' : cof_form, 'cof_form': cof_form,
'user_form' : user_form, 'user_form': user_form,
'group_form' : group_form, 'group_form': group_form,
'negative_form': negative_form, 'negative_form': negative_form,
'pwd_form' : pwd_form, 'pwd_form': pwd_form,
}) })
@permission_required('kfet.manage_perms') @permission_required('kfet.manage_perms')
@ -1703,6 +1737,7 @@ def order_create(request, pk):
articles = (Article.objects articles = (Article.objects
.filter(suppliers=supplier.pk) .filter(suppliers=supplier.pk)
.distinct()
.select_related('category') .select_related('category')
.order_by('category__name', 'name')) .order_by('category__name', 'name'))
@ -1993,6 +2028,14 @@ class JSONResponseMixin(object):
return context 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, class HybridDetailView(JSONResponseMixin,
SingleObjectTemplateResponseMixin, SingleObjectTemplateResponseMixin,
BaseDetailView): BaseDetailView):
@ -2023,14 +2066,13 @@ class HybridListView(JSONResponseMixin,
return super(HybridListView, self).render_to_response(context) return super(HybridListView, self).render_to_response(context)
class ObjectResumeStat(DetailView): class ObjectResumeStat(JSONDetailView):
""" """
Summarize all the stats of an object Summarize all the stats of an object
DOES NOT RETURN A JSON RESPONSE Handles JSONResponse
""" """
template_name = 'kfet/object_stat_resume.html'
context_object_name = '' context_object_name = ''
id_prefix = 'id_a_definir' id_prefix = ''
# nombre de vues à résumer # nombre de vues à résumer
nb_stat = 2 nb_stat = 2
# Le combienième est celui par defaut ? # Le combienième est celui par defaut ?
@ -2059,7 +2101,7 @@ class ObjectResumeStat(DetailView):
'btn': "btn_%s_%d_%d" % (self.id_prefix, 'btn': "btn_%s_%d_%d" % (self.id_prefix,
object_id, object_id,
i), i),
'url': reverse_lazy(self.stat_urls[i], 'url': reverse(self.stat_urls[i],
kwargs=dict( kwargs=dict(
self.get_object_url_kwargs(), self.get_object_url_kwargs(),
**url_kwargs[i] **url_kwargs[i]
@ -2082,7 +2124,7 @@ ID_PREFIX_ACC_BALANCE = "balance_acc"
# Un résumé de toutes les vues ArticleStatBalance # Un résumé de toutes les vues ArticleStatBalance
# NE REND PAS DE JSON # REND DU JSON
class AccountStatBalanceAll(ObjectResumeStat): class AccountStatBalanceAll(ObjectResumeStat):
model = Account model = Account
context_object_name = 'account' context_object_name = 'account'
@ -2115,9 +2157,9 @@ class AccountStatBalanceAll(ObjectResumeStat):
return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs) return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
class AccountStatBalance(HybridDetailView): class AccountStatBalance(JSONDetailView):
""" """
Returns a graph (or a JSON Response) of the evolution a the personnal Returns a JSON containing the evolution a the personnal
balance of a trigramme between timezone.now() and `nb_days` balance of a trigramme between timezone.now() and `nb_days`
ago (specified to the view as an argument) ago (specified to the view as an argument)
takes into account the Operations and the Transfers takes into account the Operations and the Transfers
@ -2126,7 +2168,6 @@ class AccountStatBalance(HybridDetailView):
model = Account model = Account
trigramme_url_kwarg = 'trigramme' trigramme_url_kwarg = 'trigramme'
nb_date_url_kwargs = 'nb_date' nb_date_url_kwargs = 'nb_date'
template_name = 'kfet/account_stat_balance.html'
context_object_name = 'account' context_object_name = 'account'
id_prefix = "" id_prefix = ""
@ -2233,10 +2274,10 @@ class AccountStatBalance(HybridDetailView):
nb_days_string = 'anytime' nb_days_string = 'anytime'
else: else:
nb_days_string = str(int(nb_days)) nb_days_string = str(int(nb_days))
context['changes'] = changes context['charts'] = [ { "color": "rgb(255, 99, 132)",
context['chart_id'] = "%s_%s_%s_days" % (self.id_prefix, "label": "Balance",
self.object.id, "values": changes } ]
nb_days_string) context['is_time_chart'] = True
context['min_date'] = changes[len(changes)-1]['at'] context['min_date'] = changes[len(changes)-1]['at']
context['max_date'] = changes[0]['at'] context['max_date'] = changes[0]['at']
# TODO: offset # TODO: offset
@ -2282,14 +2323,13 @@ class AccountStatLastAll(ObjectResumeStat):
return super(AccountStatLastAll, self).dispatch(*args, **kwargs) return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
class AccountStatLast(HybridDetailView): class AccountStatLast(JSONDetailView):
""" """
Returns a graph (or a JSON Response) of the evolution a the personnal Returns a JSON containing the evolution a the personnal
consommation of a trigramme at the diffent dates specified consommation of a trigramme at the diffent dates specified
""" """
model = Account model = Account
trigramme_url_kwarg = 'trigramme' trigramme_url_kwarg = 'trigramme'
template_name = 'kfet/account_stat_last.html'
context_object_name = 'account' context_object_name = 'account'
end_date = timezone.now() end_date = timezone.now()
id_prefix = "" id_prefix = ""
@ -2341,10 +2381,9 @@ class AccountStatLast(HybridDetailView):
operations = self.sort_operations() operations = self.sort_operations()
for i in operations: for i in operations:
nb_ventes[i] = tot_ventes(operations[i]) nb_ventes[i] = tot_ventes(operations[i])
context['nb_ventes'] = nb_ventes context['charts'] = [ { "color": "rgb(255, 99, 132)",
# ID unique "label": "NB items achetés",
context['chart_id'] = "%s_%d" % (self.id_prefix, "values": nb_ventes } ]
self.object.id)
return context return context
@method_decorator(login_required) @method_decorator(login_required)
@ -2421,13 +2460,12 @@ class ArticleStatLastAll(ObjectResumeStat):
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs) return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
class ArticleStatLast(HybridDetailView): class ArticleStatLast(JSONDetailView):
""" """
Returns a graph (or a JSON Response) of the consommation Returns a JSON containing the consommation
of an article at the diffent dates precised of an article at the diffent dates precised
""" """
model = Article model = Article
template_name = 'kfet/article_stat_last.html'
context_object_name = 'article' context_object_name = 'article'
end_date = timezone.now() end_date = timezone.now()
id_prefix = "" id_prefix = ""
@ -2488,12 +2526,15 @@ class ArticleStatLast(HybridDetailView):
operations[i] operations[i]
.exclude(group__on_acc__trigramme='LIQ') .exclude(group__on_acc__trigramme='LIQ')
) )
context['nb_ventes'] = nb_ventes context['charts'] = [ { "color": "rgb(255, 99, 132)",
context['nb_accounts'] = nb_accounts "label": "Toutes consommations",
context['nb_liq'] = nb_liq "values": nb_ventes },
# ID unique { "color": "rgb(54, 162, 235)",
context['chart_id'] = "%s_%d" % (self.id_prefix, "label": "LIQ",
self.object.id) "values": nb_liq },
{ "color": "rgb(255, 205, 86)",
"label": "Comptes K-Fêt",
"values": nb_accounts } ]
return context return context
@method_decorator(login_required) @method_decorator(login_required)