WIP: Aureplop/kpsul js refactor #501

Draft
delobell wants to merge 215 commits from aureplop/kpsul_js_refactor into master
19 changed files with 3898 additions and 1843 deletions

View file

@ -81,6 +81,7 @@ INSTALLED_APPS = [
'kfet.open',
'channels',
'widget_tweaks',
'django_js_reverse',
'custommail',
'djconfig',
'wagtail.wagtailforms',

View file

@ -6,9 +6,11 @@ from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from django.views.decorators.cache import cache_page
from django.views.generic.base import TemplateView
from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views
from django_js_reverse.views import urls_js
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
@ -91,6 +93,7 @@ urlpatterns = [
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente,
name="ml_bda_revente"),
url(r'^k-fet/', include('kfet.urls')),
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# djconfig

View file

@ -44,6 +44,11 @@
width:90px;
}
#history .opegroup .infos {
text-align:center;
width:145px;
}
#history .opegroup .valid_by {
padding-left:20px
}
@ -71,6 +76,10 @@
text-align:right;
}
#history .ope .glyphicon {
padding-left:15px;
}
#history .ope .infos2 {
padding-left:15px;
}
@ -88,11 +97,11 @@
color:#FFF;
}
#history .ope.canceled, #history .transfer.canceled {
#history [canceled="true"] {
color:#444;
}
#history .ope.canceled::before, #history.transfer.canceled::before {
#history [canceled="true"]::before {
position: absolute;
content: ' ';
width:100%;

View file

@ -49,10 +49,10 @@ input[type=number]::-webkit-outer-spin-button {
height:120px;
}
#account[data-balance="ok"] #account_form input { background:#009011; color:#FFF;}
#account[data-balance="low"] #account_form input { background:#EC6400; color:#FFF; }
#account[data-balance="neg"] #account_form input { background:#C8102E; color:#FFF; }
#account[data-balance="frozen"] #account_form input { background:#000FBA; color:#FFF; }
#account[data_balance="ok"] #account_form input { background:#009011; color:#FFF;}
#account[data_balance="low"] #account_form input { background:#EC6400; color:#FFF; }
#account[data_balance="neg"] #account_form input { background:#C8102E; color:#FFF; }
#account[data_balance="frozen"] #account_form input { background:#000FBA; color:#FFF; }
#account_form {
padding:0;
@ -90,7 +90,7 @@ input[type=number]::-webkit-outer-spin-button {
font-size:12px;
}
#account_data #account-balance {
#account_data #account-balance_ukf {
height:40px;
line-height:40px;
@ -102,6 +102,10 @@ input[type=number]::-webkit-outer-spin-button {
font-weight:bold;
}
#account-is_cof {
font-weight:bold;
}
#account .buttons {
position:absolute;
bottom:0;
@ -119,7 +123,7 @@ input[type=number]::-webkit-outer-spin-button {
font-size:14px;
line-height:24px;
}
#account_data #account-balance {
#account_data #account-balance_ukf {
font-size:50px;
line-height:60px;
height:60px;
@ -306,28 +310,40 @@ input[type=number]::-webkit-outer-spin-button {
#articles_data {
overflow:auto;
max-height:500px;
}
#articles_data table {
width: 100%;
}
#articles_data table tr.article {
#articles_data div.article {
height:25px;
font-size:14px;
}
#articles_data table tr.article td:first-child {
#articles_data .article[data_stock="low"] {
background:rgba(236,100,0,0.3);
}
#articles_data span {
height:25px;
line-height:25px;
display: inline-block;
}
#articles_data span.name {
padding-left:10px;
width:78%;
}
#articles_data table tr.article td + td {
padding-right:10px;
text-align:right;
#articles_data span.price {
width:8%;
}
#articles_data table tr.category {
#articles_data span.stock {
width:14%;
}
#articles_data div.category {
height:35px;
line-height:35px;
background-color:#c8102e;
font-family:"Roboto Slab";
font-size:16px;
@ -335,7 +351,7 @@ input[type=number]::-webkit-outer-spin-button {
color:#FFF;
}
#articles_data table tr.category>td:first-child {
#articles_data div.category>span:first-child {
padding-left:20px;
}

View file

@ -73,14 +73,19 @@
.jconfirm .capslock .glyphicon {
position: absolute;
display:none;
padding: 10px;
right: 0px;
top: 15px;
font-size: 30px;
display: none ;
margin-left: 60px !important;
}
.capslock_on .capslock .glyphicon{
display: inline-block !important;
}
.jconfirm .capslock input {
padding-right: 50px;
padding-left: 50px;

View file

@ -1,148 +1,235 @@
function KHistory(options={}) {
$.extend(this, KHistory.default_options, options);
var cancelHistory = new Event("cancel_done");
this.$container = $(this.container);
class KHistory {
this.reset = function() {
this.$container.html('');
static get default_options() {
return {
'templates': {
'purchase': '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="infos2"></span><span class="addcost"></span><span class="canceled"></span></div>',
'specialope': '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="infos2"></span><span class="addcost"></span><span class="canceled"></span></div>',
'opegroup': '<div class="opegroup"><span class="time"></span><span class="trigramme"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>',
'transfergroup': '<div class="opegroup"><span class="time"></span><span class="infos"></span><span class="valid_by"></span><span class="comment"></span></div>',
'day': '<div class="day"><span class="date"></span></div>',
'transfer': '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="glyphicon glyphicon-arrow-right"></span><span class="infos2"></span><span class="canceled"></span></div>',
},
'api_options': {
from: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss'),
},
};
}
constructor(options) {
var all_options = $.extend(true, {}, this.constructor.default_options, options);
this.api_options = all_options.api_options;
this._$container = $('#history');
this._$nb_opes = $('#nb_opes');
this.data = new OperationList();
if (!all_options.no_select)
this.selection = new KHistorySelection(this);
if (!all_options.static)
OperationWebSocket.add_handler(data => this.update_data(data));
var templates = all_options.templates;
if (all_options.no_trigramme)
templates['opegroup'] = '<div class="opegroup"><span class="time"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>';
this.display = new ForestDisplay(this._$container, templates, this.data);
this._init_events();
}
fetch(api_options) {
this.data.clear();
$.extend(this.api_options, api_options);
this.data.fromAPI(this.api_options)
.done( () => this.display_data() );
}
display_data() {
this.display.clear();
this.display.render(this.data);
var nb_opes = this._$container.find('.ope[canceled="false"]').length;
this._$nb_opes.text(nb_opes);
}
_init_events() {
var that = this;
$(document).on('keydown', function(e) {
if (e.keyCode == 46 && that.selection) {
//DEL key ; we delete the selected operations (if any)
var to_cancel = that.selection.get_selected();
if (to_cancel['opes'].length > 0 || to_cancel['transfers'].length > 0)
that.cancel_operations(to_cancel);
}
});
}
cancel_operations(to_cancel) {
var that = this;
var on_success = function() {
if (that.selection)
that.selection.reset();
$(that).trigger("cancel_done");
};
this.addOpeGroup = function(opegroup) {
var $day = this._getOrCreateDay(opegroup['at']);
var $opegroup = this._opeGroupHtml(opegroup);
$day.after($opegroup);
var trigramme = opegroup['on_acc_trigramme'];
var is_cof = opegroup['is_cof'];
for (var i=0; i<opegroup['opes'].length; i++) {
var $ope = this._opeHtml(opegroup['opes'][i], is_cof, trigramme);
$ope.data('opegroup', opegroup['id']);
$opegroup.after($ope);
}
}
this._opeHtml = function(ope, is_cof, trigramme) {
var $ope_html = $(this.template_ope);
var parsed_amount = parseFloat(ope['amount']);
var amount = amountDisplay(parsed_amount, is_cof, trigramme);
var infos1 = '', infos2 = '';
if (ope['type'] == 'purchase') {
infos1 = ope['article_nb'];
infos2 = ope['article__name'];
} else {
infos1 = parsed_amount.toFixed(2)+'€';
switch (ope['type']) {
case 'initial':
infos2 = 'Initial';
break;
case 'withdraw':
infos2 = 'Retrait';
break;
case 'deposit':
infos2 = 'Charge';
break;
case 'edit':
infos2 = 'Édition';
break;
}
}
$ope_html
.data('ope', ope['id'])
.find('.amount').text(amount).end()
.find('.infos1').text(infos1).end()
.find('.infos2').text(infos2).end();
var addcost_for = ope['addcost_for__trigramme'];
if (addcost_for) {
var addcost_amount = parseFloat(ope['addcost_amount']);
$ope_html.find('.addcost').text('('+amountDisplay(addcost_amount, is_cof)+'UKF pour '+addcost_for+')');
}
if (ope['canceled_at'])
this.cancelOpe(ope, $ope_html);
return $ope_html;
}
this.cancelOpe = function(ope, $ope = null) {
if (!$ope)
$ope = this.findOpe(ope['id']);
var cancel = 'Annulé';
var canceled_at = dateUTCToParis(ope['canceled_at']);
if (ope['canceled_by__trigramme'])
cancel += ' par '+ope['canceled_by__trigramme'];
cancel += ' le '+canceled_at.format('DD/MM/YY à HH:mm:ss');
$ope.addClass('canceled').find('.canceled').text(cancel);
}
this._opeGroupHtml = function(opegroup) {
var $opegroup_html = $(this.template_opegroup);
var at = dateUTCToParis(opegroup['at']).format('HH:mm:ss');
var trigramme = opegroup['on_acc__trigramme'];
var amount = amountDisplay(
parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme);
var comment = opegroup['comment'] || '';
$opegroup_html
.data('opegroup', opegroup['id'])
.find('.time').text(at).end()
.find('.amount').text(amount).end()
.find('.comment').text(comment).end()
.find('.trigramme').text(trigramme).end();
if (!this.display_trigramme)
$opegroup_html.find('.trigramme').remove();
if (opegroup['valid_by__trigramme'])
$opegroup_html.find('.valid_by').text('Par '+opegroup['valid_by__trigramme']);
return $opegroup_html;
}
this._getOrCreateDay = function(date) {
var at = dateUTCToParis(date);
var at_ser = at.format('YYYY-MM-DD');
var $day = this.$container.find('.day').filter(function() {
return $(this).data('date') == at_ser
});
if ($day.length == 1)
return $day;
var $day = $(this.template_day).prependTo(this.$container);
return $day.data('date', at_ser).text(at.format('D MMMM'));
}
this.findOpeGroup = function(id) {
return this.$container.find('.opegroup').filter(function() {
return $(this).data('opegroup') == id
api_with_auth({
url: Urls['kfet.kpsul.cancel_operations'](),
data: to_cancel,
on_success: on_success,
});
}
this.findOpe = function(id) {
return this.$container.find('.ope').filter(function() {
return $(this).data('ope') == id
add_node(data) {
var node = this.data.get_or_create(data.modelname, data.content, 0);
this.display.add(node);
}
update_node(modelname, id, update_data) {
var updated = this.data.update(modelname, id, update_data);
if (!updated)
return false;
this.display.update(updated);
return true;
}
is_valid(opegroup) {
var options = this.api_options;
if (options.from && dateUTCToParis(opegroup.content.at).isBefore(moment(options.from)))
return false;
if (options.to && dateUTCToParis(opegroup.content.at).isAfter(moment(options.to)))
return false;
var accounts_filter = options.accounts && options.accounts.length;
var checkouts_filter = options.checkouts && options.checkouts.length;
if (opegroup.modelname == 'opegroup') {
if (options.transfersonly)
return false;
if (accounts_filter && options.accounts.indexOf(opegroup.content.account_id) < 0)
return false;
if (checkouts_filter && options.checkouts.indexOf(opegroup.content.checkout_id) < 0)
return false;
} else if (opegroup.modelname == 'transfergroup') {
if (options.opesonly)
return false;
if (checkouts_filter)
return false;
if (accounts_filter) {
opegroup.content.children =
opegroup.content.children.filter( function(transfer) {
var is_from_in =
options.accounts.indexOf(transfer.content.from_acc_id) >= 0;
var is_to_in =
options.accounts.indexOf(transfer.content.to_acc_id) >= 0;
return is_from_in || is_to_in;
});
if (opegroup.content.children.length == 0)
return false;
}
}
return true;
}
update_data(data) {
var opegroups = data['opegroups'];
var opes = data['opes'];
for (let ope of opes) {
if (ope['cancellation']) {
let update_data = {
'canceled_at': ope.canceled_at,
'canceled_by': ope.canceled_by,
};
if (ope.modelname === 'ope') {
this.update_node('purchase', ope.id, update_data)
|| this.update_node('specialope', ope.id, update_data);
} else if (ope.modelname === 'transfer') {
this.update_node('transfer', ope.id, update_data);
}
}
}
for (let opegroup of opegroups) {
if (opegroup['cancellation']) {
let update_data = { 'amount': opegroup.amount };
this.update_node('opegroup', opegroup.id, update_data);
}
if (opegroup['add'] && this.is_valid(opegroup)) {
this.add_node(opegroup);
}
}
var nb_opes = this._$container.find('.ope[canceled="false"]').length;
$('#nb_opes').text(nb_opes);
}
}
class KHistorySelection {
constructor(history) {
this._$container = history._$container;
this._init();
}
_init() {
this._$container.selectable({
filter: 'div.opegroup, div.ope',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('opegroup')) {
$(this).parent().find('.ope').addClass('ui-selected');
}
});
},
unselected: function(e, ui) {
$(ui.unselected).each(function() {
if ($(this).hasClass('opegroup')) {
$(this).parent().find('.ope').removeClass('ui-selected');
}
});
},
});
}
this.cancelOpeGroup = function(opegroup) {
var $opegroup = this.findOpeGroup(opegroup['id']);
var trigramme = $opegroup.find('.trigramme').text();
var amount = amountDisplay(
parseFloat(opegroup['amount'], opegroup['is_cof'], trigramme));
$opegroup.find('.amount').text(amount);
get_selected() {
var selected = {'transfers': [], 'opes': [],};
this._$container.find('.ope.ui-selected').each(function() {
var [type, id] = $(this).parent().attr('id').split('-');
if (type === 'transfer')
selected['transfers'].push(id);
else
selected['opes'].push(id);
});
return selected;
}
reset() {
this._$container.find('.ui-selected')
.removeClass('.ui-selected');
}
KHistory.default_options = {
container: '#history',
template_day: '<div class="day"></div>',
template_opegroup: '<div class="opegroup"><span class="time"></span><span class="trigramme"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>',
template_ope: '<div class="ope"><span class="amount"></span><span class="infos1"></span><span class="infos2"></span><span class="addcost"></span><span class="canceled"></span></div>',
display_trigramme: true,
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,139 @@
/**
* @file Miscellaneous JS definitions for <tt>k-fet</tt> app.
* @copyright 2017 cof-geek
* @license MIT
*/
/**
* String method
* @memberof String
* @return {String} String formatted as trigramme
*/
String.prototype.formatTri = function() {
return this.toUpperCase().substr(0, 3);
}
/**
* String method
* @global
* @return {Boolean} true iff String follows trigramme pattern
*/
String.prototype.isValidTri = function() {
var pattern = /^[^a-z]{3}$/;
return pattern.test(this);
}
/**
* Checks if given argument is float ;
* if not, parses given argument to float value.
* @global
* @return {float}
*/
function floatCheck(v) {
if (typeof v === 'number')
return v;
return Number.parseFloat(v);
}
function intCheck(v) {
return Number.parseInt(v);
}
function floatCheck(v) {
if (typeof v === 'number')
return v;
return Number.parseFloat(v);
}
function booleanCheck(v) {
return v == true;
}
/**
* Short: Equivalent to python str format.
* Source: [MDN]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals}.
* @example
* var t1Closure = template`${0}${1}${0}!`;
* t1Closure('Y', 'A'); // "YAY!"
* @example
* var t2Closure = template`${0} ${'foo'}!`;
* t2Closure('Hello', {foo: 'World'}); // "Hello World!"
*/
function template(strings, ...keys) {
return (function(...values) {
var dict = values[values.length - 1] || {};
var result = [strings[0]];
keys.forEach(function(key, i) {
var value = Number.isInteger(key) ? values[key] : dict[key];
result.push(value, strings[i + 1]);
});
return result.join('');
});
}
/**
* Get and store K-Psul config from API.
* <br><br>
*
* Config should be accessed statically only.
*/
class Config {
/**
* Get or create config object.
* @private
* @return {object} object - config keys/values
*/
static _get_or_create_config() {
if (window.config === undefined)
window.config = {};
return window.config;
}
/**
* Get config from API.
* @param {jQueryAjaxComplete} [callback] - A function to be called when
* the request finishes.
*/
static reset(callback) {
$.getJSON(Urls['kfet.kpsul.get_settings']())
.done(function(data) {
for (var key in data) {
Config.set(key, data[key]);
}
})
.always(callback);
}
/**
* Get value for key in config.
* @param {string} key
*/
static get(key) {
return this._get_or_create_config()[key];
}
/**
* Set value for key in config.
* @param {string} key
* @param {*} value
*/
static set(key, value) {
// API currently returns string for Decimal type
if (['addcost_amount', 'subvention_cof'].indexOf(key) > -1)
value = floatCheck(value);
this._get_or_create_config()[key] = value;
}
}
/*
* CSRF Token
*/
@ -19,6 +154,7 @@ $.ajaxSetup({
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
function add_csrf_form($form) {
@ -28,6 +164,29 @@ function add_csrf_form($form) {
}
/*
* Capslock management
*/
window.capslock = -1;
$(document).on('keypress', function(e) {
var s = String.fromCharCode(e.which);
if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)|| //caps on, shift off
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on
$('body').addClass('capslock_on')
} else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)|| //caps off, shift off
(s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on
$('body').removeClass('capslock_on')
}
});
$(document).on('keydown', function(e) {
if (e.which == 20) {
$('body').toggleClass('capslock_on')
}
});
/*
* Generic Websocket class and k-psul ws instanciation
*/
@ -92,7 +251,7 @@ function amountDisplay(amount, is_cof=false, tri='') {
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 + Config.get('subvention_cof') / 100 : 1;
return rounding(amount * coef_cof * 10);
}
@ -101,6 +260,58 @@ function isValidTrigramme(trigramme) {
return trigramme.match(pattern);
}
/**
* Dialogs with user via jconfirm
*/
class UserDialog {
static get defaults() {
return {'title': '', 'content': ''};
}
constructor(data) {
$.extend(this, this.constructor.defaults, data);
}
open(settings) {
// Arg management
var pre_content = settings.pre_content || '';
var post_content = settings.post_content || '';
var callback = settings.callback || $.noop;
var that = this;
$.confirm({
title: this.title,
content: pre_content + this.content + post_content,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var inputs = {};
this.$content.find('input').each(function () {
inputs[$(this).attr('name')] = $(this).val();
});
if (Object.keys(inputs).length > 1)
return callback(inputs);
else
return callback(inputs[Object.keys(inputs)[0]]);
},
onOpen: function() {
var that = this
this.$content.find('input').on('keydown', function(e) {
if (e.keyCode == 13) {
e.preventDefault();
that.$confirmButton.click();
}
});
},
onClose: function() { if (settings.next_focus) { this._lastFocused = settings.next_focus; } }
});
}
}
function getErrorsHtml(data) {
var content = '';
if (!data)
@ -148,59 +359,77 @@ function getErrorsHtml(data) {
return content;
}
function requestAuth(data, callback, focus_next = null) {
var content = getErrorsHtml(data);
content += '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>',
$.confirm({
title: 'Authentification requise',
content: content,
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function() {
var password = this.$content.find('input').val();
callback(password);
});
}
var authDialog = new UserDialog({
'title': 'Authentification requise',
'content': '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>',
});
//Note/TODO: the returned ajax object can be improved by allowing chaining on errors 403/400
function api_with_auth(settings, password) {
if (window.api_lock == 1)
return false;
window.api_lock = 1;
var url = settings.url;
if (!url)
return false;
var data = settings.data || {} ;
var on_success = settings.on_success || $.noop ;
var on_400 = settings.on_400 || $.noop ;
return $.ajax({
dataType: "json",
url: url,
method: "POST",
data: data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password)
$xhr.setRequestHeader("KFetPassword", password);
},
onOpen: function() {
var that = this;
var capslock = -1 ; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown
this.$content.find('input').on('keypress', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
var s = String.fromCharCode(e.which);
if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)|| //caps on, shift off
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on
capslock = 1 ;
} else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)|| //caps off, shift off
(s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on
capslock = 0 ;
}
if (capslock == 1)
$('.capslock .glyphicon').show() ;
else if (capslock == 0)
$('.capslock .glyphicon').hide() ;
});
// Capslock key is not detected by keypress
this.$content.find('input').on('keydown', function(e) {
if (e.which == 20) {
capslock = 1-capslock ;
}
if (capslock == 1)
$('.capslock .glyphicon').show() ;
else if (capslock == 0)
$('.capslock .glyphicon').hide() ;
});
})
.done(function(data) {
on_success(data);
})
.fail(function($xhr) {
var response = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
authDialog.open({
callback: function(password) {
api_with_auth(settings, password)
},
onClose: function() {
if (focus_next)
this._lastFocused = focus_next;
pre_content: getErrorsHtml(response),
next_focus: settings.next_focus,
});
break;
case 400:
on_400(response);
break;
}
})
.always(function() {
window.api_lock = 0;
});
}
String.prototype.pluralize = function(count, irreg_plural) {
if (Math.abs(count) >= 2)
return irreg_plural ? irreg_plural : this+'s' ;
return this ;
}
/**
* Setup jquery-confirm

View file

@ -0,0 +1,678 @@
/**
* @file K-Psul JS
* @copyright 2017 cof-geek
* @license MIT
*/
class KPsulManager {
constructor(env) {
this._env = env;
this.account_manager = new AccountManager(this);
this.checkout_manager = new CheckoutManager(this);
this.article_manager = new ArticleManager(this);
this.history = new KHistory({
api_options: {'opesonly': true},
});
this._init_events();
}
reset(soft) {
soft = soft || false;
this.account_manager.reset();
this.article_manager.reset();
if (!soft) {
this.checkout_manager.reset();
this.article_manager.reset_data();
this.history.fetch();
}
return this;
}
focus() {
if (this.checkout_manager.is_empty())
this.checkout_manager.focus();
else if (this.account_manager.is_empty())
this.account_manager.focus();
else
this.article_manager.focus();
return this;
}
_init_events() {
$(this.history).on("cancel_done", () => this.reset(true).focus());
}
}
class AccountManager {
constructor() {
this._$container = $('#account');
this.account = new Account();
this.selection = new AccountSelection(this);
this.search = new AccountSearch(this);
// buttons: search, read or create
this._$buttons_container = this._$container.find('.buttons');
this._buttons_templates = {
create: template`<a href="${'url'}" class="btn btn-primary" target="_blank" title="Créer ce compte"><span class="glyphicon glyphicon-plus"></span></a>`,
read: template`<a href="${'url'}" class="btn btn-primary" target="_blank" title="Détails du compte"><span class="glyphicon glyphicon-info-sign"></span></a>`,
search: template`<button class="btn btn-primary search" title="Rechercher"><span class="glyphicon glyphicon-search"></span></button>`,
};
}
is_empty() { return this.account.is_empty(); }
display() {
this._display_data();
this._display_buttons();
}
_display_data() {
this.account.display(this._$container, {
'prefix_prop': '#account-',
});
}
_display_buttons() {
var buttons;
if (this.is_empty()) {
var trigramme = this.selection.get();
if (trigramme.isValidTri()) {
let url = Account.url_create(trigramme);
buttons = this._buttons_templates['create']({url: url});
} else { /* trigramme input is empty or invalid */
buttons = this._buttons_templates['search']();
}
} else { /* an account is loaded */
let url = this.account.url_read;
buttons = this._buttons_templates['read']({url: url});
}
this._$buttons_container.html(buttons);
}
update(trigramme) {
if (trigramme !== undefined)
this.selection.set(trigramme);
trigramme = trigramme || this.selection.get();
if (trigramme.isValidTri()) {
this.account.get_by_apipk(trigramme)
.done( () => this._update_on_success() )
.fail( () => this.reset_data() );
} else {
this.reset_data();
}
}
_update_on_success() {
$('#id_on_acc').val(this.account.id);
this.display();
kpsul.focus();
kpsul._env.updateBasketAmount();
kpsul._env.updateBasketRel();
}
reset() {
$('#id_on_acc').val(0);
this.selection.reset();
this.search.reset();
this.reset_data();
}
reset_data() {
this.account.clear();
this.display();
}
focus() {
this.selection.focus();
return this;
}
}
class AccountSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_trigramme');
this._init_events();
}
_init_events() {
var that = this;
// user change trigramme
this._$input
.on('input', () => this.manager.update());
// LIQ shortcuts
this._$input
.on('keydown', function(e) {
// keys: 13:Enter|40:Arrow-Down
if (e.keyCode == 13 || e.keyCode == 40)
that.manager.update('LIQ');
});
}
get() {
return this._$input.val().formatTri();
}
set(v) {
this._$input.val(v);
}
focus() {
this._$input.focus();
}
reset() {
this.set('');
}
}
class AccountSearch {
constructor(manager) {
this.manager = manager;
this._content = '<input type="text" name="q" id="search_autocomplete" autocomplete="off" spellcheck="false" autofocus><div id="account_results"></div>';
this._input = '#search_autocomplete';
this._results_container = '#account_results';
this._init_outer_events();
}
open() {
var that = this;
this._$dialog = $.dialog({
title: 'Recherche de compte',
content: this._content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
onOpen: function() {
that._$input = $(that._input);
that._$results_container = $(that._results_container);
that._init_form()
._init_inner_events();
},
});
}
_init_form() {
var that = this;
this._$input.yourlabsAutocomplete({
url: Urls['kfet.account.search.autocomplete'](),
minimumCharacters: 2,
id: 'search_autocomplete',
choiceSelector: '.choice',
placeholder: "Chercher un utilisateur K-Fêt",
container: that._$results_container,
box: that._$results_container,
fixPosition: function() {},
});
return this;
}
_init_outer_events() {
var that = this;
/* open search on button click */
this.manager._$container
.on('click', '.search', () => this.open());
/* open search on Ctrl-F */
this.manager._$container
.on('keydown', function(e) {
if (e.which == 70 && e.ctrlKey) {
that.open();
e.preventDefault();
}
});
}
_init_inner_events() {
this._$input.bind('selectChoice',
(e, choice, autocomplete) => this._on_select(e, choice, autocomplete)
);
return this;
}
_on_select(e, choice, autocomplete) {
this.manager.update(choice.find('.trigramme').text());
this.reset();
}
reset() {
if (this._$dialog !== undefined) {
this._$dialog.close();
}
}
}
class CheckoutManager {
constructor() {
this._$container = $('#checkout');
this.display_prefix = '#checkout-';
this.checkout = new Checkout();
this.selection = new CheckoutSelection(this);
this._$laststatement_container = $('#last_statement');
this.laststatement = new Statement();
this.laststatement_display_prefix = '#checkout-last_statement_';
this._$buttons_container = this._$container.find('.buttons');
this._buttons_templates = {
read: template`<a class="btn btn-primary" href="${'url'}" title="En savoir plus" target="_blank"><span class="glyphicon glyphicon-info-sign"></span></a>`,
statement_create: template`<a href="${'url'}" title="Effectuer un relevé" class="btn btn-primary" target="_blank"><span class="glyphicon glyphicon-euro"></span></a>`,
};
OperationWebSocket.add_handler(data => this.update_data(data));
}
update(id) {
if (id !== undefined)
this.selection.set(id);
id = id || this.selection.get();
var api_options = {
'last_statement': true,
};
this.checkout.get_by_apipk(id, api_options)
.done( (data) => this._update_on_success(data) )
.fail( () => this.reset_data() );
kpsul.focus();
}
_update_on_success(data) {
if (data['laststatement'] !== undefined)
this.laststatement.from(data['laststatement']);
$('#id_checkout').val(this.checkout.id);
this.display();
}
update_data(ws_data) {
let data = ws_data["checkouts"].find(o => o.id === this.checkout.id);
if (!data)
return;
this.checkout.update(data);
this._update_on_success(data);
}
is_empty() { return this.checkout.is_empty(); }
display() {
this._display_data();
this._display_laststatement();
this._display_buttons();
}
_display_data() {
this.checkout.display(this._$container, {
'prefix_prop': this.display_prefix,
});
}
_display_laststatement() {
if (this.laststatement.is_empty()) {
this._$laststatement_container.hide();
} else {
this.laststatement.display(this._$laststatement_container, {
'prefix_prop': this.laststatement_display_prefix
});
this._$laststatement_container.show();
}
}
_display_buttons() {
var buttons = '';
if (!this.is_empty()) {
var url_newcheckout = Statement.url_create(this.checkout.id);
buttons += this._buttons_templates['statement_create']({
url: url_newcheckout});
var url_read = this.checkout.url_read;
buttons += this._buttons_templates['read']({url: url_read});
}
this._$buttons_container.html(buttons);
}
reset() {
$('#id_checkout').val(0);
this.selection.reset();
this.reset_data();
if (this.selection.choices.length == 1)
this.update(this.selection.choices[0]);
}
reset_data() {
this.checkout.clear();
this.laststatement.clear();
this.display();
}
focus() {
this.selection.focus();
return this;
}
}
class CheckoutSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_checkout_select');
this._init_events();
this.choices =
this._$input.find('option[value!=""]')
.toArray()
.map(function(opt) {
return parseInt($(opt).attr('value'));
});
}
_init_events() {
this._$input.on('change', () => this.manager.update());
}
get() {
return this._$input.val() || 0;
}
set(v) {
this._$input.find('option[value='+ v +']').prop('selected', true);
}
reset() {
this._$input.find('option:first').prop('selected', true);
}
focus() {
this._$input.focus();
return this;
}
}
class ArticleManager {
constructor(env) {
this._env = env; // Global K-Psul Manager
this._$container = $('#articles_data');
this._$input = $('#article_autocomplete');
this._$nb = $('#article_number');
this._$stock = $('#article_stock');
this.selected = new Article();
this.data = new ArticleList();
var $container = $('#articles_data');
var templates = {
category: '<div class="category"><span class="name"></span></div>',
article: '<div class="article"><span class="name"></span><span class="price"></span><span class="stock"></span></div>',
};
this.display = new ForestDisplay($container, templates, this.data);
this.autocomplete = new ArticleAutocomplete(this, $container);
this._init_events();
OperationWebSocket.add_handler(data => this.update_data(data));
}
get nb() {
return this._$nb.val();
}
display_list() {
this.display.render(this.data);
}
validate(article) {
this.selected.from(article);
this._$input.val(article.name);
this._$nb.val('1');
this._$stock.text('/'+article.stock);
this._$nb.focus().select();
}
unset() {
this.selected.clear();
}
is_empty() {
return this.selected.is_empty();
}
reset_data() {
this.display.clear();
this.data.clear();
this.data.fromAPI()
.done( () => this.display_list() );
}
update_data(data) {
for (let article_dict of data.articles) {
var updated = this.data.update('article', article_dict.id, article_dict);
if (updated) {
this.display.update(updated);
}
}
}
reset() {
this.unset();
this._$stock.text('');
this._$nb.val('');
this._$input.val('');
this.autocomplete.showAll();
}
_init_events() {
var that = this;
// 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12
var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/;
var arrowKeys = /^(37|38|39|40)$/;
//Global input event (to merge ?)
this._$input.on('keydown', function(e) {
if (e.keyCode == 13 && that._$input.val() == '') {
kpsul._env.performOperations();
}
});
this._$container.on('click', '.article', function() {
var id = $(this).parent().attr('id').split('-')[1];
var article = that.data.find('article', id);
if (article)
that.validate(article);
});
this._$nb.on('keydown', function(e) {
if (e.keyCode == 13 && that.constructor.check_nb(that.nb) && !that.is_empty()) {
kpsul._env.addPurchase(that.selected, that.nb);
that.reset();
that.focus();
}
if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.ctrlKey) {
if (e.ctrlKey && e.charCode == 65)
that._$nb.val('');
return true;
}
if (that.constructor.check_nb(that.nb+e.key))
return true;
return false;
});
}
//Note : this function may not be needed after the whole rework
get_article(id) {
return this.data.find('article', id);
}
focus() {
if (this.is_empty())
this._$input.focus();
else
this._$nb.focus();
return this;
}
static check_nb(nb) {
return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24;
}
}
class ArticleAutocomplete {
constructor(article_manager, $container) {
this.manager = article_manager;
this._$container = $container;
this._$input = $('#article_autocomplete');
this.showAll();
this._init_events();
}
_init_events() {
var that = this;
// 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12
var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/;
var arrowKeys = /^(37|38|39|40)$/;
this._$input
.on('keydown', function(e) {
var text = that._$input.val();
if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.ctrlKey) {
// For the backspace key, we suppose the cursor is at the very end
if(e.keyCode == 8) {
that.update(text.substring(0, text.length-1), true);
}
return true;
}
that.update(text+e.key, false);
return false;
});
}
update(prefix, backspace) {
this.resetMatch();
var article_list = this.manager.data;
var lower = prefix.toLowerCase();
var that = this;
article_list.traverse('article', function(article) {
if (article.name.toLowerCase().startsWith(lower))
that.matching.push(article);
});
if (this.matching.length == 1) {
if (!backspace) {
this.manager.validate(this.matching[0]);
this.showAll();
} else {
this.manager.unset();
this.updateDisplay();
}
} else if (this.matching.length > 1) {
this.manager.unset();
this.updateDisplay();
if (!backspace)
this.updatePrefix();
}
}
updateDisplay() {
var that = this;
this.manager.data.traverse('category', function(category) {
var is_active = false;
for (let article of category.articles) {
if (that.matching.indexOf(article) != -1) {
is_active = true;
that._$container.find('#article-'+article.id).show();
} else {
that._$container.find('#article-'+article.id).hide();
}
}
if (is_active) {
that._$container.find('#category-'+category.id).show();
} else {
that._$container.find('#category-'+category.id).hide();
}
});
}
updatePrefix() {
var lower = this.matching.map(function (article) {
return article.name.toLowerCase();
});
lower.sort();
var first = lower[0], last = lower[lower.length-1],
length = first.length, i = 0;
while (i < length && first.charAt(i) === last.charAt(i)) i++;
this._$input.val(first.substring(0,i));
}
showAll() {
var that = this;
this.resetMatch();
this.manager.data.traverse('article', function(article) {
that.matching.push(article);
});
this.updateDisplay();
}
resetMatch() {
this.matching = [];
}
}

View file

@ -4,6 +4,7 @@
{% load l10n %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.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>
@ -85,33 +86,11 @@ $(document).ready(function() {
<script type="text/javascript">
$(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})}
'use strict';
khistory = new KHistory({
display_trigramme: false,
});
var khistory = new KHistory({'no_trigramme': true});
function getHistory() {
var data = {
'accounts': [{{ account.pk }}],
}
$.ajax({
dataType: "json",
url : "{% url 'kfet.history.json' %}",
method : "POST",
data : data,
})
.done(function(data) {
for (var i=0; i<data['opegroups'].length; i++) {
khistory.addOpeGroup(data['opegroups'][i]);
}
var nb_opes = khistory.$container.find('.ope:not(.canceled)').length;
$('#nb_opes').text(nb_opes);
});
}
getHistory();
Config.reset(() => khistory.fetch({'accounts': [{{account.pk}}]}));
});
</script>

View file

@ -30,6 +30,7 @@
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.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="{% url 'js_reverse' %}"></script>
{% include "kfetopen/init.html" %}

View file

@ -3,6 +3,7 @@
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/multiple-select.js' %}"></script>
{{ filter_form.media }}
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
@ -32,14 +33,14 @@
{% block main %}
<table id="history" class="table">
</table>
<div id="history">
</div>
<script type="text/javascript">
$(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})}
'use strict';
khistory = new KHistory();
var khistory = new KHistory();
var $from_date = $('#id_from_date');
var $to_date = $('#id_to_date');
@ -54,7 +55,9 @@ $(document).ready(function() {
return selected;
}
function getHistory() {
function updateHistory() {
// Get API options
var data = {};
if ($from_date.val())
data['from'] = moment($from_date.val()).format('YYYY-MM-DD HH:mm:ss');
@ -64,21 +67,11 @@ $(document).ready(function() {
if ($checkouts)
data['checkouts'] = checkouts;
var accounts = getSelectedMultiple($accounts);
data['accounts'] = accounts;
if (accounts)
data['accounts'] = accounts.map(id => parseInt(id));
$.ajax({
dataType: "json",
url : "{% url 'kfet.history.json' %}",
method : "POST",
data : data,
})
.done(function(data) {
for (var i=0; i<data['opegroups'].length; i++) {
khistory.addOpeGroup(data['opegroups'][i]);
}
var nb_opes = khistory.$container.find('.ope:not(.canceled)').length;
$('#nb_opes').text(nb_opes);
});
// Update history
khistory.fetch(data);
}
let defaults_datetimepicker = {
@ -92,8 +85,9 @@ $(document).ready(function() {
$from_date.datetimepicker($.extend({}, defaults_datetimepicker, {
defaultDate: moment().subtract(24, 'hours'),
}));
$to_date.datetimepicker($.extend({}, defaults_datetimepicker, {
defaultDate: moment(),
defaultDate: moment().add(5, 'minutes') // workaround for 'stepping' rounding
}));
$("#from_date").on("dp.change", function (e) {
@ -111,131 +105,11 @@ $(document).ready(function() {
countSelected: "# sur %"
});
$("input").on('dp.change change', function() {
khistory.reset();
getHistory();
$("#update_history").on('click', function() {
updateHistory();
});
khistory.$container.selectable({
filter: 'div.opegroup, div.ope',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('opegroup')) {
var opegroup = $(this).data('opegroup');
$(this).siblings('.ope').filter(function() {
return $(this).data('opegroup') == opegroup
}).addClass('ui-selected');
}
});
},
});
$(document).on('keydown', function (e) {
if (e.keyCode == 46) {
// DEL (Suppr)
var opes_to_cancel = [];
khistory.$container.find('.ope.ui-selected').each(function () {
opes_to_cancel.push($(this).data('ope'));
});
if (opes_to_cancel.length > 0)
confirmCancel(opes_to_cancel);
}
});
function confirmCancel(opes_to_cancel) {
var nb = opes_to_cancel.length;
var content = nb+" opérations vont être annulées";
$.confirm({
title: 'Confirmation',
content: content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function() {
cancelOperations(opes_to_cancel);
}
});
}
function requestAuth(data, callback) {
var content = getErrorsHtml(data);
content += '<input type="password" name="password" autofocus>',
$.confirm({
title: 'Authentification requise',
content: content,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var password = this.$content.find('input').val();
callback(password);
},
onOpen: function() {
var that = this;
this.$content.find('input').on('keypress', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
});
},
});
}
function getErrorsHtml(data) {
var content = '';
if ('missing_perms' in data['errors']) {
content += 'Permissions manquantes';
content += '<ul>';
for (var i=0; i<data['errors']['missing_perms'].length; i++)
content += '<li>'+data['errors']['missing_perms'][i]+'</li>';
content += '</ul>';
}
if ('negative' in data['errors']) {
var url_base = "{% url 'kfet.account.update' LIQ}";
url_base = base_url(0, url_base.length-8);
for (var i=0; i<data['errors']['negative'].length; i++) {
content += '<a class="btn btn-primary" href="'+url_base+data['errors']['negative'][i]+'/edit" target="_blank">Autorisation de négatif requise pour '+data['errors']['negative'][i]+'</a>';
}
}
return content;
}
function cancelOperations(opes_array, password = '') {
var data = { 'operations' : opes_array }
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.cancel_operations' %}",
method : "POST",
data : data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password != '')
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
khistory.$container.find('.ui-selected').removeClass('ui-selected');
})
.fail(function($xhr) {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, function(password) {
cancelOperations(opes_array, password);
});
break;
case 400:
displayErrors(getErrorsHtml(data));
break;
}
});
}
getHistory();
Config.reset(updateHistory);
});
</script>

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,22 @@
{% extends 'kfet/base_col_2.html' %}
{% load staticfiles %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %}
{% block title %}Transferts{% endblock %}
{% block header-title %}Transferts{% endblock %}
{% block fixed %}
<aside>
<div class="heading">
<div id="nb_opes"></div>
<div class="sub">transferts</div>
</div>
</aside>
<div class="buttons">
<a class="btn btn-primary" href="{% url 'kfet.transfers.create' %}">
Nouveaux
@ -16,109 +27,17 @@
{% block main %}
<div id="history">
{% for transfergroup in transfergroups %}
<div class="opegroup transfergroup" data-transfergroup="{{ transfergroup.pk }}">
<span>{{ transfergroup.at }}</span>
<span>{{ transfergroup.valid_by.trigramme }}</span>
<span>{{ transfergroup.comment }}</span>
</div>
{% for transfer in transfergroup.transfers.all %}
<div class="ope transfer{% if transfer.canceled_at %} canceled{% endif %}" data-transfer="{{ transfer.pk }}" data-transfergroup="{{ transfergroup.pk }}">
<span class="amount">{{ transfer.amount }} €</span>
<span class="from_acc">{{ transfer.from_acc.trigramme }}</span>
<span class="glyphicon glyphicon-arrow-right"></span>
<span class="to_acc">{{ transfer.to_acc.trigramme }}</span>
</div>
{% endfor %}
{% endfor %}
<div id="history" class="table">
</div>
<script type="text/javascript">
$(document).ready(function() {
'use strict';
lock = 0;
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
function cancelTransfers(transfers_array, password = '') {
if (lock == 1)
return false
lock = 1;
var data = { 'transfers' : transfers_array }
$.ajax({
dataType: "json",
url : "{% url 'kfet.transfers.cancel' %}",
method : "POST",
data : data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password != '')
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
for (var i=0; i<data['canceled'].length; i++) {
$('#history').find('.transfer[data-transfer='+data['canceled'][i]+']')
.addClass('canceled');
}
$('#history').find('.ui-selected').removeClass('ui-selected');
lock = 0;
})
.fail(function($xhr) {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, function(password) {
cancelTransfers(transfers_array, password);
});
break;
case 400:
displayErrors(getErrorsHtml(data));
break;
}
lock = 0;
});
}
$('#history').selectable({
filter: 'div.transfergroup, div.transfer',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('transfergroup')) {
var transfergroup = $(this).attr('data-transfergroup');
$(this).siblings('.ope').filter(function() {
return $(this).attr('data-transfergroup') == transfergroup
}).addClass('ui-selected');
}
});
},
});
$(document).on('keydown', function (e) {
if (e.keyCode == 46) {
// DEL (Suppr)
var transfers_to_cancel = [];
$('#history').find('.transfer.ui-selected').each(function () {
transfers_to_cancel.push($(this).attr('data-transfer'));
});
if (transfers_to_cancel.length > 0)
cancelTransfers(transfers_to_cancel);
}
});
var khistory = new KHistory();
Config.reset(() => khistory.fetch({'transfersonly': true}));
});
</script>

View file

@ -3,6 +3,7 @@
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}">
<script type="text/javascript" src="{% url 'js_reverse' %}"></script>
{% endblock %}
{% block title %}Nouveaux transferts{% endblock %}
@ -51,14 +52,12 @@
<script type="text/javascript">
$(document).ready(function () {
function getAccountData(trigramme, callback = function() {}) {
$.ajax({
dataType: "json",
url : "{% url 'kfet.account.read.json' %}",
method : "POST",
data : { trigramme: trigramme },
success : callback,
});
function getAccountData(trigramme, callback) {
callback = callback || $.noop;
$.getJSON(Urls['kfet.account.read'](trigramme), {
'format': 'json',
})
.done(callback);
}
function updateAccountData(trigramme, $input) {

View file

@ -4,6 +4,7 @@ from decimal import Decimal
from unittest import mock
from django.contrib.auth.models import Group
from django.core.serializers.json import DjangoJSONEncoder
from django.core.urlresolvers import reverse
from django.test import Client, TestCase
from django.utils import timezone
@ -12,7 +13,7 @@ from ..config import kfet_config
from ..models import (
Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory,
InventoryArticle, Operation, OperationGroup, Order, OrderArticle, Supplier,
SupplierArticle, Transfer, TransferGroup,
SupplierArticle, TransferGroup,
)
from .testcases import ViewTestCaseMixin
from .utils import create_team, create_user, get_perms
@ -248,6 +249,26 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase):
r = client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_json(self):
r = self.client.get(self.url, {'format': 'json'})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
expected = {
'name': 'first last',
'trigramme': '001',
'balance': '0.00',
}
self.assertDictContainsSubset(expected, content)
self.assertSetEqual(set(content.keys()), set([
'id', 'trigramme', 'first_name', 'last_name', 'name', 'email',
'is_cof', 'promo', 'balance', 'is_frozen', 'departement',
'nickname',
]))
class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.update'
@ -762,6 +783,35 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200)
self.assertEqual(r.context['checkout'], self.checkout)
def test_json(self):
r = self.client.get(self.url, {
'format': 'json',
'last_statement': '1',
})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
json_now = json.dumps(self.now, cls=DjangoJSONEncoder).strip('"')
json_tomorrow = json.dumps(
self.now + timedelta(days=1),
cls=DjangoJSONEncoder
).strip('"')
self.assertEqual(content, {
'id': self.checkout.pk,
'name': 'Checkout',
'balance': '10.00',
'valid_from': json_now,
'valid_to': json_tomorrow,
'laststatement': {
'id': self.checkout.statements.all()[0].pk,
'at': json_now,
'balance_new': '10.00',
'balance_old': '10.00',
'by': str(self.accounts['team']),
},
})
class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.checkout.update'
@ -1411,46 +1461,6 @@ class KPsulViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200)
class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.kpsul.checkout_data'
url_expected = '/k-fet/k-psul/checkout_data'
http_methods = ['POST']
auth_user = 'team'
auth_forbidden = [None, 'user']
def setUp(self):
super().setUp()
self.checkout = Checkout.objects.create(
name='Checkout',
balance=Decimal('10'),
created_by=self.accounts['team'],
valid_from=self.now,
valid_to=self.now + timedelta(days=5),
)
def test_ok(self):
r = self.client.post(self.url, {'pk': self.checkout.pk})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
expected = {
'name': 'Checkout',
'balance': '10.00',
}
self.assertDictContainsSubset(expected, content)
self.assertSetEqual(set(content.keys()), set([
'balance', 'id', 'name', 'valid_from', 'valid_to',
'last_statement_at', 'last_statement_balance',
'last_statement_by_first_name', 'last_statement_by_last_name',
'last_statement_by_trigramme',
]))
class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.kpsul.perform_operations'
url_expected = '/k-fet/k-psul/perform_operations'
@ -1470,11 +1480,82 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
http_methods = ['POST']
auth_user = 'team'
auth_user = 'team1'
auth_forbidden = [None, 'user']
def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.perform_negative_operations',
]),
'u1': create_user('user1', '001'),
'u2': create_user('user2', '002'),
'u3': create_user('user3', '003'),
}
def setUp(self):
super().setUp()
trg1 = TransferGroup.objects.create()
self.trg1_1 = trg1.transfers.create(
from_acc=self.accounts['u1'],
to_acc=self.accounts['u2'],
amount='3.5',
)
self.trg1_2 = trg1.transfers.create(
from_acc=self.accounts['u2'],
to_acc=self.accounts['u3'],
amount='2.4',
)
trg2 = TransferGroup.objects.create(
at=(
timezone.now() -
(kfet_config.cancel_duration + timedelta(seconds=1))
),
)
self.trg2_1 = trg2.transfers.create(
from_acc=self.accounts['u1'],
to_acc=self.accounts['u2'],
amount='5',
)
def test_ok(self):
pass
data = {
'transfers[]': [str(self.trg1_1.pk), str(self.trg1_2.pk)],
}
r = self.client.post(self.url, data)
self.assertEqual(r.status_code, 200)
u1 = self.accounts['u1']
u1.refresh_from_db()
self.assertEqual(u1.balance, Decimal('3.5'))
u2 = self.accounts['u2']
u2.refresh_from_db()
self.assertEqual(u2.balance, Decimal('-1.1'))
u3 = self.accounts['u3']
u3.refresh_from_db()
self.assertEqual(u3.balance, Decimal('-2.4'))
def test_error_tooold(self):
data = {
'transfers[]': [str(self.trg2_1.pk)],
}
r = self.client.post(self.url, data)
self.assertEqual(r.status_code, 403)
self.assertDictEqual(json.loads(r.content.decode('utf-8')), {
'canceled': {},
'warnings': {},
'errors': {
'missing_perms': ['Annuler des commandes non récentes'],
},
})
class KPsulArticlesData(ViewTestCaseMixin, TestCase):
@ -1486,16 +1567,10 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase):
def setUp(self):
super().setUp()
category = ArticleCategory.objects.create(name='Catégorie')
self.article1 = Article.objects.create(
category=category,
name='Article 1',
)
self.article2 = Article.objects.create(
category=category,
name='Article 2',
price=Decimal('2.5'),
)
self.ac = ArticleCategory.objects.create(name='Catégorie')
self.a1 = self.ac.articles.create(name='Article 1')
self.a2 = self.ac.articles.create(name='Article 2', price='2.5')
def test_ok(self):
r = self.client.get(self.url)
@ -1503,24 +1578,35 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase):
content = json.loads(r.content.decode('utf-8'))
articles = content['articles']
expected_list = [{
'category__name': 'Catégorie',
self.assertEqual(content, {
'objects': {
'article': [
{
'id': self.a1.pk,
'name': 'Article 1',
'price': '0.00',
}, {
'category__name': 'Catégorie',
'stock': 0,
'category__id': self.a1.category.pk,
},
{
'id': self.a2.pk,
'name': 'Article 2',
'price': '2.50',
}]
for expected, article in zip(expected_list, articles):
self.assertDictContainsSubset(expected, article)
self.assertSetEqual(set(article.keys()), set([
'id', 'name', 'price', 'stock',
'category_id', 'category__name', 'category__has_addcost',
]))
'stock': 0,
'category__id': self.a2.category.pk,
},
],
},
'related': {
'category': [
{
'id': self.ac.pk,
'name': 'Catégorie',
'has_addcost': True,
},
],
},
})
class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase):
@ -1586,34 +1672,6 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200)
class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.read.json'
url_expected = '/k-fet/accounts/read.json'
http_methods = ['POST']
auth_user = 'team'
auth_forbidden = [None, 'user']
def test_ok(self):
r = self.client.post(self.url, {'trigramme': '000'})
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode('utf-8'))
expected = {
'name': 'first last',
'trigramme': '000',
'balance': '0.00',
}
self.assertDictContainsSubset(expected, content)
self.assertSetEqual(set(content.keys()), set([
'balance', 'departement', 'email', 'id', 'is_cof', 'is_frozen',
'name', 'nickname', 'promo', 'trigramme',
]))
class SettingsListViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.settings'
url_expected = '/k-fet/settings/'
@ -1766,62 +1824,6 @@ class TransferPerformViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(team1.balance, Decimal('2.4'))
class TransferCancelViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.transfers.cancel'
url_expected = '/k-fet/transfers/cancel'
http_methods = ['POST']
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
# Convenience
'kfet.perform_negative_operations',
]),
}
@property
def post_data(self):
return {
'transfers[]': [self.transfer1.pk, self.transfer2.pk],
}
def setUp(self):
super().setUp()
group = TransferGroup.objects.create()
self.transfer1 = Transfer.objects.create(
group=group,
from_acc=self.accounts['user'],
to_acc=self.accounts['team'],
amount='3.5',
)
self.transfer2 = Transfer.objects.create(
group=group,
from_acc=self.accounts['team'],
to_acc=self.accounts['root'],
amount='2.4',
)
def test_ok(self):
r = self.client.post(self.url, self.post_data)
self.assertEqual(r.status_code, 200)
user = self.accounts['user']
user.refresh_from_db()
self.assertEqual(user.balance, Decimal('3.5'))
team = self.accounts['team']
team.refresh_from_db()
self.assertEqual(team.balance, Decimal('-1.1'))
root = self.accounts['root']
root.refresh_from_db()
self.assertEqual(root.balance, Decimal('-2.4'))
class InventoryListViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.inventory'
url_expected = '/k-fet/inventaires/'

View file

@ -159,8 +159,6 @@ urlpatterns = [
# -----
url('^k-psul/$', views.kpsul, name='kfet.kpsul'),
url('^k-psul/checkout_data$', views.kpsul_checkout_data,
name='kfet.kpsul.checkout_data'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations,
name='kfet.kpsul.perform_operations'),
url('^k-psul/cancel_operations$', views.kpsul_cancel_operations,
@ -178,9 +176,6 @@ urlpatterns = [
url(r'^history.json$', views.history_json,
name='kfet.history.json'),
url(r'^accounts/read.json$', views.account_read_json,
name='kfet.account.read.json'),
# -----
# Settings urls
@ -202,8 +197,6 @@ urlpatterns = [
name='kfet.transfers.create'),
url(r'^transfers/perform$', views.perform_transfers,
name='kfet.transfers.perform'),
url(r'^transfers/cancel$', views.cancel_transfers,
name='kfet.transfers.cancel'),
# -----
# Inventories urls

View file

@ -15,7 +15,7 @@ from django.contrib.auth.models import User, Permission
from django.http import JsonResponse, Http404
from django.forms import formset_factory
from django.db import transaction
from django.db.models import F, Sum, Prefetch, Count
from django.db.models import Q, F, Sum, Prefetch, Count
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.decorators import method_decorator
@ -47,12 +47,36 @@ from decimal import Decimal
import heapq
import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
from .auth.views import ( # noqa
account_group, login_generic, AccountGroupCreate, AccountGroupUpdate,
)
# 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
def put_cleaned_data_in_dict(dict, form):
for field in form.cleaned_data:
dict[field] = form.cleaned_data[field]
@ -323,6 +347,13 @@ def account_read(request, trigramme):
request.user != account.user):
raise PermissionDenied
if request.GET.get('format') == 'json':
export_keys = ['id', 'trigramme', 'first_name', 'last_name', 'name',
'email', 'is_cof', 'promo', 'balance', 'is_frozen',
'departement', 'nickname']
data = {k: getattr(account, k) for k in export_keys}
return JsonResponse(data)
addcosts = (
OperationGroup.objects
.filter(opes__addcost_for=account,
@ -338,6 +369,7 @@ def account_read(request, trigramme):
'addcosts': addcosts,
})
# Account - Update
@ -530,18 +562,38 @@ class CheckoutCreate(SuccessMessageMixin, CreateView):
return super().form_valid(form)
# Checkout - Read
class CheckoutRead(DetailView):
class CheckoutRead(JSONResponseMixin, DetailView):
model = Checkout
template_name = 'kfet/checkout_read.html'
context_object_name = 'checkout'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['statements'] = context['checkout'].statements.order_by('-at')
checkout = self.object
if self.request.GET.get('last_statement'):
context['laststatement'] = checkout.statements.latest('at')
else:
context['statements'] = checkout.statements.order_by('-at')
return context
def render_to_response(self, context, **kwargs):
if self.request.GET.get('format') == 'json':
export_keys = ['id', 'name', 'balance', 'valid_from', 'valid_to']
data = {k: getattr(self.object, k) for k in export_keys}
if 'laststatement' in context:
last_st = context['laststatement']
export_keys = ['id', 'at', 'balance_new', 'balance_old']
last_st_data = {k: getattr(last_st, k) for k in export_keys}
last_st_data['by'] = str(last_st.by)
data['laststatement'] = last_st_data
return self.render_to_json_response(data)
else:
return super().render_to_response(context, **kwargs)
# Checkout - Update
class CheckoutUpdate(SuccessMessageMixin, UpdateView):
@ -625,7 +677,24 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
form.instance.balance_new = getAmountBalance(form.cleaned_data)
form.instance.checkout_id = self.kwargs['pk_checkout']
form.instance.by = self.request.user.profile.account_kfet
return super().form_valid(form)
res = super(CheckoutStatementCreate, self).form_valid(form)
ws_data = {
'id': self.object.id,
'at': self.object.at,
'balance_new': self.object.balance_new,
'balance_old': self.object.balance_old,
'by': str(self.object.by),
}
consumers.KPsul.group_send('kfet.kpsul', {
'checkouts': [{
'id': self.object.checkout.id,
'laststatement': ws_data,
}],
})
return res
class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
model = CheckoutStatement
@ -848,50 +917,6 @@ def kpsul_get_settings(request):
return JsonResponse(data)
@teamkfet_required
def account_read_json(request):
trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme)
data = { 'id': account.pk, 'name': account.name, 'email': account.email,
'is_cof': account.is_cof, 'promo': account.promo,
'balance': account.balance, 'is_frozen': account.is_frozen,
'departement': account.departement, 'nickname': account.nickname,
'trigramme': account.trigramme }
return JsonResponse(data)
@teamkfet_required
def kpsul_checkout_data(request):
pk = request.POST.get('pk', 0)
if not pk:
pk = 0
data = (
Checkout.objects
.annotate(
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'),
last_statement_by_trigramme=F('statements__by__trigramme'),
last_statement_balance=F('statements__balance_new'),
last_statement_at=F('statements__at'))
.select_related(
'statements'
'statements__by',
'statements__by__cofprofile__user')
.filter(pk=pk)
.order_by('statements__at')
.values(
'id', 'name', 'balance', 'valid_from', 'valid_to',
'last_statement_balance', 'last_statement_at',
'last_statement_by_trigramme', 'last_statement_by_last_name',
'last_statement_by_first_name')
.last()
)
if data is None:
raise Http404
return JsonResponse(data)
@teamkfet_required
def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST)
@ -1081,31 +1106,47 @@ def kpsul_perform_operations(request):
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'modelname': 'opegroup',
'content': {
'id': operationgroup.pk,
'amount': operationgroup.amount,
'checkout__name': operationgroup.checkout.name,
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
'valid_by__trigramme': (operationgroup.valid_by and
'valid_by': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme,
'opes': [],
'trigramme': operationgroup.on_acc.trigramme,
# Used to filter websocket updates
'account_id': operationgroup.on_acc.pk,
'checkout_id': operationgroup.checkout.pk,
'children': [],
},
}]
for operation in operations:
for ope in operations:
ope_data = {
'id': operation.pk, 'type': operation.type,
'amount': operation.amount,
'addcost_amount': operation.addcost_amount,
'addcost_for__trigramme': (
operation.addcost_for and addcost_for.trigramme or None),
'article__name': (
operation.article and operation.article.name or None),
'article_nb': operation.article_nb,
'group_id': operationgroup.pk,
'canceled_by__trigramme': None, 'canceled_at': None,
'content': {
'id': ope.id,
'amount': ope.amount,
'canceled_at': None,
'canceled_by': None,
},
}
websocket_data['opegroups'][0]['opes'].append(ope_data)
if ope.type == Operation.PURCHASE:
ope_data['modelname'] = 'purchase'
ope_data['content'].update({
'article_name': ope.article.name,
'article_nb': ope.article_nb,
'addcost_amount': ope.addcost_amount,
'addcost_for':
ope.addcost_for and ope.addcost_for.trigramme or None,
})
else:
ope_data['modelname'] = 'specialope'
ope_data['content'].update({
'type': ope.type,
})
websocket_data['opegroups'][0]['content']['children'].append(ope_data)
# Need refresh from db cause we used update on queryset
operationgroup.checkout.refresh_from_db()
websocket_data['checkouts'] = [{
@ -1128,37 +1169,63 @@ def kpsul_perform_operations(request):
@teamkfet_required
def kpsul_cancel_operations(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
data = {'canceled': {}, 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (opes_pk not int or not existing)
try:
# Set pour virer les doublons
opes_post = set(map(int, filter(None, request.POST.getlist('operations[]', []))))
opes_post = (
set(map(int, filter(None, request.POST.getlist('opes[]', []))))
)
transfers_post = (
set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
)
except ValueError:
return JsonResponse(data, status=400)
opes_all = (
Operation.objects
.select_related('group', 'group__on_acc', 'group__on_acc__negative')
.filter(pk__in=opes_post))
opes_pk = [ope.pk for ope in opes_all]
opes_notexisting = [ope for ope in opes_post if ope not in opes_pk]
transfers_all = (
Transfer.objects
.select_related('group', 'from_acc', 'from_acc__negative',
'to_acc', 'to_acc__negative')
.filter(pk__in=transfers_post))
transfers_pk = [transfer.pk for transfer in transfers_all]
transfers_notexisting = [transfer for transfer in transfers_post
if transfer not in transfers_pk]
if transfers_notexisting or opes_notexisting:
if transfers_notexisting:
data['errors']['transfers_notexisting'] = transfers_notexisting
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
return JsonResponse(data, status=400)
opes_already_canceled = [] # Déjà annulée
already_canceled = {} # Opération/Transfert déjà annulé
opes = [] # Pas déjà annulée
transfers = []
required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles
# Modifs à faire sur les balances des comptes
to_accounts_balances = defaultdict(lambda: 0)
# ------ sur les montants des groupes d'opé
to_groups_amounts = defaultdict(lambda: 0)
# ------ sur les balances de caisses
to_checkouts_balances = defaultdict(lambda: 0)
# ------ sur les stocks d'articles
to_articles_stocks = defaultdict(lambda: 0)
for ope in opes_all:
if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response
opes_already_canceled.append(ope.pk)
already_canceled['opes'].append(ope.pk)
else:
opes.append(ope.pk)
# Si opé il y a plus de CANCEL_DURATION, permission requise
@ -1185,7 +1252,8 @@ def kpsul_cancel_operations(request):
# par `.save()`, amount_error est recalculé automatiquement,
# ce qui n'est pas le cas en faisant un update sur queryset
# TODO ? : Maj les balance_old de relevés pour modifier l'erreur
last_statement = (CheckoutStatement.objects
last_statement = \
(CheckoutStatement.objects
.filter(checkout=ope.group.checkout)
.order_by('at')
.last())
@ -1204,16 +1272,34 @@ def kpsul_cancel_operations(request):
# Note : si InventoryArticle est maj par .save(), stock_error
# est recalculé automatiquement
if ope.article and ope.article_nb:
last_stock = (InventoryArticle.objects
last_stock = (
InventoryArticle.objects
.select_related('inventory')
.filter(article=ope.article)
.order_by('inventory__at')
.last())
.last()
)
if not last_stock or last_stock.inventory.at < ope.group.at:
to_articles_stocks[ope.article] += ope.article_nb
if not opes:
data['warnings']['already_canceled'] = opes_already_canceled
for transfer in transfers_all:
if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response
already_canceled['transfers'].append(transfer.pk)
else:
transfers.append(transfer.pk)
# Si transfer il y a plus de CANCEL_DURATION, permission requise
if transfer.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
to_accounts_balances[transfer.from_acc] += transfer.amount
to_accounts_balances[transfer.to_acc] += -transfer.amount
if not opes and not transfers:
data['warnings']['already_canceled'] = already_canceled
return JsonResponse(data)
negative_accounts = []
@ -1240,6 +1326,10 @@ def kpsul_cancel_operations(request):
with transaction.atomic():
(Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
(Transfer.objects.filter(pk__in=transfers)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
(
Account.objects
@ -1261,11 +1351,13 @@ def kpsul_cancel_operations(request):
stock=F('stock') + to_articles_stocks[article])
# Websocket data
websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] }
websocket_data = {'opegroups': [], 'opes': [],
'checkouts': [], 'articles': []}
# Need refresh from db cause we used update on querysets
opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts]
opegroups = (OperationGroup.objects
.values('id','amount','is_cof').filter(pk__in=opegroups_pk))
.values('id', 'amount', 'is_cof')
.filter(pk__in=opegroups_pk))
for opegroup in opegroups:
websocket_data['opegroups'].append({
'cancellation': True,
@ -1273,18 +1365,29 @@ def kpsul_cancel_operations(request):
'amount': opegroup['amount'],
'is_cof': opegroup['is_cof'],
})
canceled_by__trigramme = canceled_by and canceled_by.trigramme or None
canceled_by = canceled_by and canceled_by.trigramme or None
for ope in opes:
websocket_data['opes'].append({
'cancellation': True,
'modelname': 'ope',
'id': ope,
'canceled_by__trigramme': canceled_by__trigramme,
'canceled_by': canceled_by,
'canceled_at': canceled_at,
})
for ope in transfers:
websocket_data['opes'].append({
'cancellation': True,
'modelname': 'transfer',
'id': ope,
'canceled_by': canceled_by,
'canceled_at': canceled_at,
})
# Need refresh from db cause we used update on querysets
checkouts_pk = [checkout.pk for checkout in to_checkouts_balances]
checkouts = (Checkout.objects
.values('id', 'balance').filter(pk__in=checkouts_pk))
.values('id', 'balance')
.filter(pk__in=checkouts_pk))
for checkout in checkouts:
websocket_data['checkouts'].append({
'id': checkout['id'],
@ -1298,92 +1401,195 @@ def kpsul_cancel_operations(request):
'stock': article['stock']})
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
data['canceled'] = opes
if opes_already_canceled:
data['warnings']['already_canceled'] = opes_already_canceled
data['canceled']['opes'] = opes
data['canceled']['transfers'] = transfers
if already_canceled:
data['warnings']['already_canceled'] = already_canceled
return JsonResponse(data)
@login_required
def history_json(request):
# Récupération des paramètres
from_date = request.POST.get('from', None)
to_date = request.POST.get('to', None)
limit = request.POST.get('limit', None);
checkouts = request.POST.getlist('checkouts[]', None)
accounts = request.POST.getlist('accounts[]', None)
from_date = request.GET.get('from', None)
to_date = request.GET.get('to', None)
checkouts = request.GET.getlist('checkouts[]', None)
accounts = request.GET.getlist('accounts[]', None)
transfers_only = request.GET.get('transfersonly', None)
opes_only = request.GET.get('opesonly', None)
# Un non-membre de l'équipe n'a que accès à son historique
if not request.user.has_perm('kfet.is_team'):
accounts = [request.user.profile.account_kfet]
# Construction de la requête (sur les opérations) pour le prefetch
queryset_prefetch = Operation.objects.select_related(
'article', 'canceled_by', 'addcost_for')
ope_queryset_prefetch = Operation.objects.select_related(
'canceled_by', 'addcost_for', 'article')
ope_prefetch = Prefetch('opes',
queryset=ope_queryset_prefetch)
transfer_queryset_prefetch = Transfer.objects.select_related(
'from_acc', 'to_acc', 'canceled_by')
if accounts:
transfer_queryset_prefetch = transfer_queryset_prefetch.filter(
Q(from_acc__in=accounts) |
Q(to_acc__in=accounts))
if not request.user.has_perm('kfet.is_team'):
acc = request.user.profile.account_kfet
transfer_queryset_prefetch = transfer_queryset_prefetch.filter(
Q(from_acc=acc) | Q(to_acc=acc))
transfer_prefetch = Prefetch('transfers',
queryset=transfer_queryset_prefetch,
to_attr='filtered_transfers')
# Construction de la requête principale
opegroups = (
OperationGroup.objects
.prefetch_related(Prefetch('opes', queryset=queryset_prefetch))
.select_related('on_acc', 'valid_by')
.prefetch_related(ope_prefetch)
.select_related('on_acc',
'valid_by')
.order_by('at')
)
transfergroups = (
TransferGroup.objects
.prefetch_related(transfer_prefetch)
.select_related('valid_by')
.order_by('at')
)
# Application des filtres
if from_date:
opegroups = opegroups.filter(at__gte=from_date)
transfergroups = transfergroups.filter(at__gte=from_date)
if to_date:
opegroups = opegroups.filter(at__lt=to_date)
transfergroups = transfergroups.filter(at__lt=to_date)
if checkouts:
opegroups = opegroups.filter(checkout_id__in=checkouts)
transfergroups = TransferGroup.objects.none()
if transfers_only:
opegroups = OperationGroup.objects.none()
if opes_only:
transfergroups = TransferGroup.objects.none()
if accounts:
opegroups = opegroups.filter(on_acc_id__in=accounts)
# Un non-membre de l'équipe n'a que accès à son historique
if not request.user.has_perm('kfet.is_team'):
opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet)
if limit:
opegroups = opegroups[:limit]
opegroups = opegroups.filter(on_acc__in=accounts)
# Construction de la réponse
opegroups_list = []
related_data = defaultdict(list)
objects_data = defaultdict(list)
for opegroup in opegroups:
opegroup_dict = {
'id': opegroup.id,
'amount': opegroup.amount,
'at': opegroup.at,
'checkout_id': opegroup.checkout_id,
'is_cof': opegroup.is_cof,
'comment': opegroup.comment,
'opes' : [],
'on_acc__trigramme':
'trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
}
if request.user.has_perm('kfet.is_team'):
opegroup_dict['valid_by__trigramme'] = (
opegroup_dict['valid_by'] = (
opegroup.valid_by and opegroup.valid_by.trigramme or None)
for ope in opegroup.opes.all():
ope_dict = {
'id': ope.id,
'type' : ope.type,
'amount': ope.amount,
'article_nb' : ope.article_nb,
'addcost_amount': ope.addcost_amount,
'canceled_at': ope.canceled_at,
'article__name':
ope.article and ope.article.name or None,
'addcost_for__trigramme':
ope.addcost_for and ope.addcost_for.trigramme or None,
'is_cof': opegroup.is_cof,
'trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
'opegroup__id': opegroup.id,
}
if request.user.has_perm('kfet.is_team'):
ope_dict['canceled_by__trigramme'] = (
ope_dict['canceled_by'] = (
ope.canceled_by and ope.canceled_by.trigramme or None)
opegroup_dict['opes'].append(ope_dict)
opegroups_list.append(opegroup_dict)
return JsonResponse({ 'opegroups': opegroups_list })
if ope.type == Operation.PURCHASE:
ope_dict.update({
'article_name': ope.article.name,
'article_nb': ope.article_nb,
'addcost_amount': ope.addcost_amount,
'addcost_for':
ope.addcost_for and ope.addcost_for.trigramme or None,
})
objects_data['purchase'].append(ope_dict)
else:
ope_dict.update({
'type': ope.type,
})
objects_data['specialope'].append(ope_dict)
related_data['opegroup'].append(opegroup_dict)
for transfergroup in transfergroups:
if transfergroup.filtered_transfers:
transfergroup_dict = {
'id': transfergroup.id,
'at': transfergroup.at,
'comment': transfergroup.comment,
}
if request.user.has_perm('kfet.is_team'):
transfergroup_dict['valid_by'] = (
transfergroup.valid_by and
transfergroup.valid_by.trigramme or
None)
for transfer in transfergroup.filtered_transfers:
transfer_dict = {
'id': transfer.id,
'amount': transfer.amount,
'canceled_at': transfer.canceled_at,
'from_acc': transfer.from_acc.trigramme,
'to_acc': transfer.to_acc.trigramme,
'transfergroup__id': transfergroup.id,
}
if request.user.has_perm('kfet.is_team'):
transfer_dict['canceled_by'] = (
transfer.canceled_by and
transfer.canceled_by.trigramme or
None)
objects_data['transfer'].append(transfer_dict)
related_data['transfergroup'].append(transfergroup_dict)
data = {
'objects': objects_data,
'related': related_data,
}
return JsonResponse(data)
@teamkfet_required
def kpsul_articles_data(request):
articles = (
Article.objects
.values('id', 'name', 'price', 'stock', 'category_id',
'category__name', 'category__has_addcost')
.filter(is_sold=True))
return JsonResponse({ 'articles': list(articles) })
data = {'objects': {}, 'related': {}}
data['objects']['article'] = [
{
'id': article.id,
'name': article.name,
'price': article.price,
'stock': article.stock,
'category__id': article.category_id,
}
for article in Article.objects.filter(is_sold=True)
]
data['related']['category'] = [
{
'id': category.id,
'name': category.name,
'has_addcost': category.has_addcost,
}
for category in ArticleCategory.objects.all()
]
return JsonResponse(data)
@teamkfet_required
@ -1430,25 +1636,10 @@ config_update = (
# Transfer views
# -----
@teamkfet_required
def transfers(request):
transfers_pre = Prefetch(
'transfers',
queryset=(
Transfer.objects
.select_related('from_acc', 'to_acc')
),
)
transfergroups = (
TransferGroup.objects
.select_related('valid_by')
.prefetch_related(transfers_pre)
.order_by('-at')
)
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
return render(request, 'kfet/transfers.html')
@teamkfet_required
@ -1457,6 +1648,7 @@ def transfers_create(request):
return render(request, 'kfet/transfers_create.html',
{ 'transfer_formset': transfer_formset })
@teamkfet_required
def perform_transfers(request):
data = {'errors': {}, 'transfers': [], 'transfergroup': 0}
@ -1464,13 +1656,16 @@ def perform_transfers(request):
# Checking transfer_formset
transfer_formset = TransferFormSet(request.POST)
if not transfer_formset.is_valid():
return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400)
return JsonResponse({'errors': list(transfer_formset.errors)},
status=400)
transfers = transfer_formset.save(commit=False)
# Initializing vars
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts
# Required perms to perform all transfers
required_perms = set(['kfet.add_transfer'])
# For balances of accounts
to_accounts_balances = defaultdict(lambda: 0)
for transfer in transfers:
to_accounts_balances[transfer.from_acc] -= transfer.amount
@ -1519,8 +1714,8 @@ def perform_transfers(request):
negative = AccountNegative(
account=account, start=timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
elif (hasattr(account, 'negative') and
not account.negative.balance_offset):
account.negative.delete()
# Saving transfer group
@ -1533,103 +1728,38 @@ def perform_transfers(request):
transfer.save()
data['transfers'].append(transfer.pk)
# Websocket data
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'modelname': 'transfergroup',
'content': {
'id': transfergroup.pk,
'at': transfergroup.at,
'comment': transfergroup.comment,
'valid_by__trigramme': (transfergroup.valid_by and
transfergroup.valid_by.trigramme or None),
'children': []
},
}]
for transfer in transfers:
ope_data = {
'modelname': 'transfer',
'content': {
'id': transfer.pk,
'amount': transfer.amount,
'from_acc': transfer.from_acc.trigramme,
'to_acc': transfer.to_acc.trigramme,
'canceled_by__trigramme': None, 'canceled_at': None,
'from_acc_id': transfer.from_acc.id,
'to_acc_id': transfer.to_acc.id,
},
}
websocket_data['opegroups'][0]['content']['children'].append(ope_data)
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
return JsonResponse(data)
@teamkfet_required
def cancel_transfers(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (transfers_pk not int or not existing)
try:
# Set pour virer les doublons
transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
except ValueError:
return JsonResponse(data, status=400)
transfers_all = (
Transfer.objects
.select_related('group', 'from_acc', 'from_acc__negative',
'to_acc', 'to_acc__negative')
.filter(pk__in=transfers_post))
transfers_pk = [ transfer.pk for transfer in transfers_all ]
transfers_notexisting = [ transfer for transfer in transfers_post
if transfer not in transfers_pk ]
if transfers_notexisting:
data['errors']['transfers_notexisting'] = transfers_notexisting
return JsonResponse(data, status=400)
transfers_already_canceled = [] # Déjà annulée
transfers = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
for transfer in transfers_all:
if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response
transfers_already_canceled.append(transfer.pk)
else:
transfers.append(transfer.pk)
# Si transfer il y a plus de CANCEL_DURATION, permission requise
if transfer.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
to_accounts_balances[transfer.from_acc] += transfer.amount
to_accounts_balances[transfer.to_acc] += -transfer.amount
if not transfers:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
negative_accounts = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = negative_accounts
return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None
canceled_at = timezone.now()
with transaction.atomic():
(Transfer.objects.filter(pk__in=transfers)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
if not account.negative.start:
account.negative.start = timezone.now()
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
account.negative.delete()
data['canceled'] = transfers
if transfers_already_canceled:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
class InventoryList(ListView):
queryset = (Inventory.objects
@ -2020,29 +2150,6 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
# ---------------
# 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):

View file

@ -18,6 +18,7 @@ statistics==1.0.3.5
django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3
django-js-reverse==0.7.3
channels==1.1.5
python-dateutil
wagtail==1.10.*