WIP: Aureplop/kpsul js refactor #501

Draft
delobell wants to merge 215 commits from aureplop/kpsul_js_refactor into master
11 changed files with 1542 additions and 841 deletions
Showing only changes of commit 311e0c48bd - Show all commits

View file

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

View file

@ -1,148 +1,215 @@
function KHistory(options={}) { var cancelHistory = new Event("cancel_done");
$.extend(this, KHistory.default_options, options);
this.$container = $(this.container); class KHistory {
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'),
},
this.reset = function() {
this.$container.html('');
}; };
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) { constructor(options) {
var $ope_html = $(this.template_ope); var all_options = $.extend({}, this.constructor.default_options, options);
var parsed_amount = parseFloat(ope['amount']); this.api_options = all_options.api_options;
var amount = amountDisplay(parsed_amount, is_cof, trigramme);
var infos1 = '', infos2 = '';
if (ope['type'] == 'purchase') { this._$container = $('#history');
infos1 = ope['article_nb']; this._$nb_opes = $('#nb_opes');
infos2 = ope['article__name'];
} else { this.data = new OperationList();
infos1 = parsed_amount.toFixed(2)+'€';
switch (ope['type']) { if (!all_options.no_select)
case 'initial': this.selection = new KHistorySelection(this);
infos2 = 'Initial';
break; if (!all_options.static)
case 'withdraw': OperationWebSocket.add_handler(data => this.update_data(data));
infos2 = 'Retrait';
break; var templates = all_options.templates
case 'deposit': if (all_options.no_trigramme)
infos2 = 'Charge'; templates['opegroup'] =
break; '<div class="opegroup"><span class="time"></span><span class="amount"></span><span class="valid_by"></span><span class="comment"></span></div>';
case 'edit':
infos2 = 'Édition'; this.display = new ForestDisplay(this._$container, templates, this.data);
break;
} this._init_events();
} }
$ope_html fetch(api_options) {
.data('ope', ope['id']) this.data.clear();
.find('.amount').text(amount).end()
.find('.infos1').text(infos1).end() $.extend(this.api_options, api_options);
.find('.infos2').text(infos2).end();
this.data.fromAPI(this.api_options)
.done( () => this.display_data() );
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']) display_data() {
this.cancelOpe(ope, $ope_html); this.display.clear();
this.display.render(this.data);
return $ope_html; var nb_opes = this._$container.find('.ope[canceled="false"]').length;
this._$nb_opes.text(nb_opes);
} }
this.cancelOpe = function(ope, $ope = null) { _init_events() {
if (!$ope) var that = this;
$ope = this.findOpe(ope['id']); $(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();
var cancel = 'Annulé'; if (to_cancel['opes'].length > 0 || to_cancel['transfers'].length > 0)
var canceled_at = dateUTCToParis(ope['canceled_at']); that.cancel_operations(to_cancel);
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
}); });
} }
this.findOpe = function(id) { cancel_operations(to_cancel) {
return this.$container.find('.ope').filter(function() { var that = this ;
return $(this).data('ope') == id var on_success = function() {
}); if (that.selection)
that.selection.reset() ;
$(that).trigger("cancel_done");
} }
this.cancelOpeGroup = function(opegroup) { api_with_auth({
var $opegroup = this.findOpeGroup(opegroup['id']); url: Urls['kfet.kpsul.cancel_operations'](),
var trigramme = $opegroup.find('.trigramme').text(); data: to_cancel,
var amount = amountDisplay( on_success: on_success,
parseFloat(opegroup['amount'], opegroup['is_cof'], trigramme)); })
$opegroup.find('.amount').text(amount);
} }
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.at).isBefore(moment(options.from)))
return false;
if (options.to && dateUTCToParis(opegroup.at).isAfter(moment(options.to)))
return false;
if (options.transfersonly && opegroup.constructor.verbose_name == 'opegroup')
return false;
if (options.opesonly && opegroup.constructor.verbose_name == 'transfergroup')
return false;
if (options.accounts && options.accounts.length &&
options.accounts.indexOf(opegroup.account_id) < 0)
return false;
if (options.checkouts && options.checkouts.length &&
(opegroup.modelname == 'transfergroup' ||
options.checkouts.indexOf(opegroup.checkout_id) < 0))
return false;
return true;
}
update_data(data) {
var opegroups = data['opegroups'];
var opes = data['opes'];
for (let ope of opes) {
if (ope['cancellation']) {
var 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']) {
var 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);
}
} }
KHistory.default_options = { class KHistorySelection {
container: '#history',
template_day: '<div class="day"></div>', constructor(history) {
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>', this._$container = history._$container;
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>', this._init();
display_trigramme: true, }
_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');
}
});
},
});
}
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');
}
} }

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,20 @@ String.prototype.isValidTri = function() {
} }
/**
* 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) { function intCheck(v) {
return Number.parseInt(v); return Number.parseInt(v);
} }
@ -340,11 +354,24 @@ function getErrorsHtml(data) {
return content; return content;
} }
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
var authDialog = new UserDialog({ var authDialog = new UserDialog({
'title': 'Authentification requise', 'title': 'Authentification requise',
'content': '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>', '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) { function api_with_auth(settings, password) {
if (window.api_lock == 1) if (window.api_lock == 1)
return false; return false;
@ -357,7 +384,7 @@ function api_with_auth(settings, password) {
var on_success = settings.on_success || $.noop ; var on_success = settings.on_success || $.noop ;
var on_400 = settings.on_400 || $.noop ; var on_400 = settings.on_400 || $.noop ;
$.ajax({ return $.ajax({
dataType: "json", dataType: "json",
url: url, url: url,
method: "POST", method: "POST",
@ -392,3 +419,9 @@ function api_with_auth(settings, password) {
window.api_lock = 0; 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 ;
}

View file

@ -12,6 +12,11 @@ class KPsulManager {
this.account_manager = new AccountManager(this); this.account_manager = new AccountManager(this);
this.checkout_manager = new CheckoutManager(this); this.checkout_manager = new CheckoutManager(this);
this.article_manager = new ArticleManager(this); this.article_manager = new ArticleManager(this);
this.history = new KHistory({
api_options: {'opesonly': true},
});
this._init_events();
} }
reset(soft) { reset(soft) {
@ -23,6 +28,7 @@ class KPsulManager {
if (!soft) { if (!soft) {
this.checkout_manager.reset(); this.checkout_manager.reset();
this.article_manager.reset_data(); this.article_manager.reset_data();
this.history.fetch();
} }
} }
@ -37,6 +43,14 @@ class KPsulManager {
return this; return this;
} }
_init_events() {
var that = this ;
$(this.history).on("cancel_done", function(e) {
that.reset(true);
that.focus();
});
}
} }
@ -414,14 +428,19 @@ class ArticleManager {
this._$input = $('#article_autocomplete'); this._$input = $('#article_autocomplete');
this._$nb = $('#article_number'); this._$nb = $('#article_number');
this._$stock = $('#article_stock'); this._$stock = $('#article_stock');
this.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.selected = new Article() ; this.selected = new Article() ;
this.list = new ArticleList() ; this.data = new ArticleList() ;
this.autocomplete = new ArticleAutocomplete(this); 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(); this._init_events();
OperationWebSocket.add_handler(data => this.update_data(data));
} }
get nb() { get nb() {
@ -429,7 +448,7 @@ class ArticleManager {
} }
display_list() { display_list() {
this.list.display(this._$container, this.templates) ; this.display.render(this.data);
} }
validate(article) { validate(article) {
@ -449,25 +468,18 @@ class ArticleManager {
} }
reset_data() { reset_data() {
this._$container.html(''); this.display.clear();
this.list.clear(); this.data.clear();
this.list.fromAPI() this.data.fromAPI()
.done( () => this.display_list() ); .done( () => this.display_list() );
} }
get_article(id) {
return this.list.find('article', id);
}
update_data(data) { update_data(data) {
for (let article_dict of data.articles) { for (let article_dict of data.articles) {
var article = this.get_article(article_dict.id);
// For now, article additions are disregarded var updated = this.data.update('article', article_dict.id, article_dict);
if (article) { if (updated) {
article.stock = article_dict.stock; this.display.update(updated);
this._$container.find('#article-'+article.id+' .stock')
.text(article.stock);
} }
} }
} }
@ -496,7 +508,7 @@ class ArticleManager {
this._$container.on('click', '.article', function() { this._$container.on('click', '.article', function() {
var id = $(this).parent().attr('id').split('-')[1]; var id = $(this).parent().attr('id').split('-')[1];
var article = that.list.find('article', id); var article = that.data.find('article', id);
if (article) if (article)
that.validate(article); that.validate(article);
}); });
@ -507,18 +519,26 @@ class ArticleManager {
that.reset(); that.reset();
that.focus(); that.focus();
} }
if (normalKeys.test(e.keyCode) || arrowKeys.test(e.KeyCode) || e.ctrlKey) {
if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.ctrlKey) {
if (e.ctrlKey && e.charCode == 65) if (e.ctrlKey && e.charCode == 65)
that._$nb.val(''); that._$nb.val('');
return true ; return true ;
} }
if (that.constructor.check_nb(that.nb+e.key)) if (that.constructor.check_nb(that.nb+e.key))
return true; return true;
return false; return false;
}); });
} }
//Note : this function may not be needed after the whole rework
get_article(id) {
return this.data.find('article', id) ;
}
focus() { focus() {
if (this.is_empty()) if (this.is_empty())
this._$input.focus(); this._$input.focus();
@ -535,9 +555,9 @@ class ArticleManager {
class ArticleAutocomplete { class ArticleAutocomplete {
constructor(article_manager) { constructor(article_manager, $container) {
this.manager = article_manager; this.manager = article_manager;
this._$container = article_manager._$container ; this._$container = $container ;
this._$input = $('#article_autocomplete'); this._$input = $('#article_autocomplete');
this.showAll() ; this.showAll() ;
@ -549,11 +569,12 @@ class ArticleAutocomplete {
// 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12 // 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 normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/;
var arrowKeys = /^(37|38|39|40)$/;
this._$input this._$input
.on('keydown', function(e) { .on('keydown', function(e) {
var text = that._$input.val() ; var text = that._$input.val() ;
if (normalKeys.test(e.keyCode) || e.ctrlKey) { if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.ctrlKey) {
// For the backspace key, we suppose the cursor is at the very end // For the backspace key, we suppose the cursor is at the very end
if(e.keyCode == 8) { if(e.keyCode == 8) {
that.update(text.substring(0, text.length-1), true); that.update(text.substring(0, text.length-1), true);
@ -571,7 +592,7 @@ class ArticleAutocomplete {
update(prefix, backspace) { update(prefix, backspace) {
this.resetMatch(); this.resetMatch();
var article_list = this.manager.list ; var article_list = this.manager.data ;
var lower = prefix.toLowerCase() ; var lower = prefix.toLowerCase() ;
var that = this ; var that = this ;
@ -599,7 +620,7 @@ class ArticleAutocomplete {
updateDisplay() { updateDisplay() {
var that = this; var that = this;
this.manager.list.traverse('category', function(category) { this.manager.data.traverse('category', function(category) {
var is_active = false; var is_active = false;
for (let article of category.articles) { for (let article of category.articles) {
if (that.matching.indexOf(article) != -1) { if (that.matching.indexOf(article) != -1) {
@ -634,7 +655,7 @@ class ArticleAutocomplete {
showAll() { showAll() {
var that = this; var that = this;
this.resetMatch(); this.resetMatch();
this.manager.list.traverse('article', function(article) { this.manager.data.traverse('article', function(article) {
that.matching.push(article); that.matching.push(article);
}); });
this.updateDisplay(); this.updateDisplay();

View file

@ -4,10 +4,16 @@
{% load l10n %} {% load l10n %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script> <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/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% url 'js_reverse' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% if account.user == request.user %} {% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
@ -94,33 +100,11 @@ $(document).ready(function() {
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})} 'use strict';
khistory = new KHistory({ var khistory = new KHistory({'no_trigramme': true});
display_trigramme: false,
});
function getHistory() { Config.reset(() => khistory.fetch({'accounts': [{{account.pk}}]}));
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();
}); });
</script> </script>

View file

@ -7,6 +7,7 @@
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/bootstrap-datetimepicker.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/bootstrap-datetimepicker.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/multiple-select.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
@ -38,6 +39,9 @@
<div class="line"><b>Comptes</b> {{ filter_form.accounts }}</div> <div class="line"><b>Comptes</b> {{ filter_form.accounts }}</div>
</div> </div>
</div> </div>
<div class="buttons">
<a id="update_history" class="btn btn-primary btn-lg">Mettre à jour</a>
</div>
</div> </div>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="col-sm-8 col-md-9 col-content-right">
@ -51,8 +55,8 @@
<div class="content-right-block"> <div class="content-right-block">
<h2>Opérations</h2> <h2>Opérations</h2>
<div> <div>
<table id="history" class="table"> <div id="history">
</table> </div>
</div> </div>
</div> </div>
</div> </div>
@ -61,9 +65,9 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})} 'use strict';
khistory = new KHistory(); var khistory = new KHistory();
var $from_date = $('#from_date'); var $from_date = $('#from_date');
var $to_date = $('#to_date'); var $to_date = $('#to_date');
@ -78,7 +82,9 @@ $(document).ready(function() {
return selected; return selected;
} }
function getHistory() { function updateHistory() {
// Get API options
var data = {}; var data = {};
if ($from_date.val()) if ($from_date.val())
data['from'] = moment($from_date.val()).format('YYYY-MM-DD HH:mm:ss'); data['from'] = moment($from_date.val()).format('YYYY-MM-DD HH:mm:ss');
@ -90,19 +96,8 @@ $(document).ready(function() {
var accounts = getSelectedMultiple($accounts); var accounts = getSelectedMultiple($accounts);
data['accounts'] = accounts; data['accounts'] = accounts;
$.ajax({ // Update history
dataType: "json", khistory.fetch(data);
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);
});
} }
$('#from_date').datetimepicker({ $('#from_date').datetimepicker({
@ -117,7 +112,7 @@ $(document).ready(function() {
timeZone : 'Europe/Paris', timeZone : 'Europe/Paris',
format : 'YYYY-MM-DD HH:mm', format : 'YYYY-MM-DD HH:mm',
stepping : 5, stepping : 5,
defaultDate: moment(), defaultDate: moment().add(5, 'minutes'), // workaround for 'stepping' rounding
locale : 'fr', locale : 'fr',
showTodayButton: true, showTodayButton: true,
}); });
@ -133,69 +128,11 @@ $(document).ready(function() {
filter: true, filter: true,
}); });
$("input").on('dp.change change', function() { $("#update_history").on('click', function() {
khistory.reset(); updateHistory();
getHistory();
}); });
khistory.$container.selectable({ Config.reset(updateHistory);
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 cancelOperations(opes_array) {
var data = { 'operations' : opes_array }
api_with_auth({
url: Urls['kfet.kpsul.cancel_operations'](),
data: data,
on_success: function(data) {
khistory.$container.find('.ui-selected').removeClass('ui-selected');
},
on_400: function(data) {
displayErrors(getErrorsHtml(data));
},
});
}
getHistory();
}); });
</script> </script>

View file

@ -156,9 +156,6 @@
</div> </div>
</div> </div>
<div id="history2">
</div>
<form id="operationgroup_form" style="display:none;">{{ operationgroup_form }}</form> <form id="operationgroup_form" style="display:none;">{{ operationgroup_form }}</form>
<form id="operation_formset" style="display:none;">{{ operation_formset }}</form> <form id="operation_formset" style="display:none;">{{ operation_formset }}</form>
@ -199,21 +196,6 @@ $(document).ready(function() {
$('#id_comment').val(''); $('#id_comment').val('');
} }
// -----
// Errors ajax
// -----
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
// ----- // -----
// Perform operations // Perform operations
// ----- // -----
@ -246,33 +228,6 @@ $(document).ready(function() {
performOperations(); performOperations();
}); });
// -----
// Cancel operations
// -----
var cancelButton = $('#cancel_operations');
var cancelForm = $('#cancel_form');
function cancelOperations(opes_array) {
var data = { 'operations' : opes_array }
api_with_auth({
url: Urls['kfet.kpsul.cancel_operations'](),
data: data,
on_success: function() {
coolReset();
},
on_400: function(response) {
displayErrors(getErrorsHtml(response));
},
next_focus: kpsul.account_manager,
});
}
// Event listeners
cancelButton.on('click', function() {
cancelOperations();
});
// ----- // -----
// Basket // Basket
// ----- // -----
@ -311,7 +266,7 @@ $(document).ready(function() {
var article_basket_html = $(item_basket_default_html); var article_basket_html = $(item_basket_default_html);
article_basket_html article_basket_html
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(nb).end() .find('.number').text('('+nb+'/'+article.stock+')').end()
.find('.name').text(article.name).end() .find('.name').text(article.name).end()
.find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof)); .find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof));
basket_container.prepend(article_basket_html); basket_container.prepend(article_basket_html);
@ -634,29 +589,6 @@ $(document).ready(function() {
.find('#id_form-'+opeindex+'-DELETE').prop('checked', !nb); .find('#id_form-'+opeindex+'-DELETE').prop('checked', !nb);
} }
// -----
// History
// -----
var khistory = new KHistory();
function getHistory() {
var data = {
from: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss'),
};
$.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 previousop_container = $('#previous_op'); var previousop_container = $('#previous_op');
function updatePreviousOp() { function updatePreviousOp() {
@ -716,69 +648,6 @@ $(document).ready(function() {
} }
// -----
// Cancel from history
// -----
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)
cancelOperations(opes_to_cancel);
}
});
// -----
// Synchronization
// -----
OperationWebSocket.add_handler(function(data) {
for (var i=0; i<data['opegroups'].length; i++) {
if (data['opegroups'][i]['add']) {
khistory.addOpeGroup(data['opegroups'][i]);
} else if (data['opegroups'][i]['cancellation']) {
khistory.cancelOpeGroup(data['opegroups'][i]);
}
}
for (var i=0; i<data['opes'].length; i++) {
if (data['opes'][i]['cancellation']) {
khistory.cancelOpe(data['opes'][i]);
}
}
for (var i=0; i<data['checkouts'].length; i++) {
if (checkout_data['id'] == data['checkouts'][i]['id']) {
checkout_data['balance'] = data['checkouts'][i]['balance'];
displayCheckoutData();
}
}
kpsul.article_manager.update_data(data);
if (data['addcost']) {
Config.set('addcost_for', data['addcost']['for']);
Config.set('addcost_amount', data['addcost']['amount']);
displayAddcost();
}
});
// ----- // -----
// General // General
// ----- // -----
@ -798,11 +667,10 @@ $(document).ready(function() {
coolReset(give_tri_focus); coolReset(give_tri_focus);
kpsul.checkout_manager.reset(); kpsul.checkout_manager.reset();
resetPreviousOp(); resetPreviousOp();
khistory.reset();
Config.reset(function() { Config.reset(function() {
kpsul.article_manager.reset_data(); kpsul.article_manager.reset_data();
displayAddcost(); displayAddcost();
getHistory(); kpsul.history.fetch();
}); });
} }
@ -882,10 +750,23 @@ $(document).ready(function() {
updateBasketAmount: updateBasketAmount, updateBasketAmount: updateBasketAmount,
updateBasketRel: updateBasketRel, updateBasketRel: updateBasketRel,
performOperations: performOperations, performOperations: performOperations,
coolReset: coolReset,
}; };
window.kpsul = new KPsulManager(env); window.kpsul = new KPsulManager(env);
// -----
// Synchronization
// -----
OperationWebSocket.add_handler(function(data) {
if (data['addcost']) {
Config.set('addcost_for', data['addcost']['for']);
Config.set('addcost_amount', data['addcost']['amount']);
displayAddcost();
}
});
hardReset(); hardReset();
}); });

View file

@ -4,9 +4,15 @@
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}"> <link rel="stylesheet" style="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<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="{% url 'js_reverse' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %} {% endblock %}
{% block title %}Transferts{% endblock %} {% block title %}Transferts{% endblock %}
@ -18,6 +24,8 @@
<div class="col-sm-4 col-md-3 col-content-left"> <div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left"> <div class="content-left">
<div class="content-left-top"> <div class="content-left-top">
<div class="line line-big" id="nb_opes"></div>
<div class="line line-bigsub">transferts</div>
</div> </div>
<div class="buttons"> <div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.transfers.create' %}"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.transfers.create' %}">
@ -31,23 +39,8 @@
<div class="content-right"> <div class="content-right">
<div class="content-right-block"> <div class="content-right-block">
<h2>Liste des transferts</h2> <h2>Liste des transferts</h2>
<div id="history"> <div id="history" class="table">
{% 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> </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 %}
</table>
</div> </div>
</div> </div>
</div> </div>
@ -56,88 +49,11 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
'use strict';
lock = 0; var khistory = new KHistory();
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);
}
});
Config.reset(() => khistory.fetch({'transfersonly': true}));
}); });
</script> </script>

View file

@ -203,8 +203,6 @@ urlpatterns = [
name='kfet.transfers.create'), name='kfet.transfers.create'),
url(r'^transfers/perform$', views.perform_transfers, url(r'^transfers/perform$', views.perform_transfers,
name='kfet.transfers.perform'), name='kfet.transfers.perform'),
url(r'^transfers/cancel$', views.cancel_transfers,
name='kfet.transfers.cancel'),
# ----- # -----
# Inventories urls # Inventories urls

View file

@ -18,7 +18,7 @@ from django.contrib.auth.models import User, Permission, Group
from django.http import JsonResponse, Http404 from django.http import JsonResponse, Http404
from django.forms import formset_factory from django.forms import formset_factory
from django.db import transaction 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.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
@ -1158,31 +1158,47 @@ def kpsul_perform_operations(request):
websocket_data = {} websocket_data = {}
websocket_data['opegroups'] = [{ websocket_data['opegroups'] = [{
'add': True, 'add': True,
'modelname': 'opegroup',
'content': {
'id': operationgroup.pk, 'id': operationgroup.pk,
'amount': operationgroup.amount, 'amount': operationgroup.amount,
'checkout__name': operationgroup.checkout.name,
'at': operationgroup.at, 'at': operationgroup.at,
'is_cof': operationgroup.is_cof, 'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment, 'comment': operationgroup.comment,
'valid_by__trigramme': (operationgroup.valid_by and 'valid_by': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None), operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme, 'trigramme': operationgroup.on_acc.trigramme,
'opes': [], # 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 = { ope_data = {
'id': operation.pk, 'type': operation.type, 'content': {
'amount': operation.amount, 'id': ope.id,
'addcost_amount': operation.addcost_amount, 'amount': ope.amount,
'addcost_for__trigramme': ( 'canceled_at': None,
operation.addcost_for and addcost_for.trigramme or None), 'canceled_by': 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,
} }
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 # Need refresh from db cause we used update on queryset
operationgroup.checkout.refresh_from_db() operationgroup.checkout.refresh_from_db()
websocket_data['checkouts'] = [{ websocket_data['checkouts'] = [{
@ -1205,37 +1221,63 @@ def kpsul_perform_operations(request):
@teamkfet_required @teamkfet_required
def kpsul_cancel_operations(request): def kpsul_cancel_operations(request):
# Pour la réponse # Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}} data = {'canceled': {}, 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (opes_pk not int or not existing) # Checking if BAD REQUEST (opes_pk not int or not existing)
try: try:
# Set pour virer les doublons # 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: except ValueError:
return JsonResponse(data, status=400) return JsonResponse(data, status=400)
opes_all = ( opes_all = (
Operation.objects Operation.objects
.select_related('group', 'group__on_acc', 'group__on_acc__negative') .select_related('group', 'group__on_acc', 'group__on_acc__negative')
.filter(pk__in=opes_post)) .filter(pk__in=opes_post))
opes_pk = [ ope.pk for ope in opes_all ] opes_pk = [ope.pk for ope in opes_all]
opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ] 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: if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting data['errors']['opes_notexisting'] = opes_notexisting
return JsonResponse(data, status=400) 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 opes = [] # Pas déjà annulée
transfers = []
required_perms = set() required_perms = set()
stop_all = False stop_all = False
cancel_duration = kfet_config.cancel_duration cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé to_accounts_balances = defaultdict(lambda: 0)
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses # ------ sur les montants des groupes d'opé
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles 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: for ope in opes_all:
if ope.canceled_at: if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response # 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: else:
opes.append(ope.pk) opes.append(ope.pk)
# Si opé il y a plus de CANCEL_DURATION, permission requise # Si opé il y a plus de CANCEL_DURATION, permission requise
@ -1262,7 +1304,8 @@ def kpsul_cancel_operations(request):
# par `.save()`, amount_error est recalculé automatiquement, # par `.save()`, amount_error est recalculé automatiquement,
# ce qui n'est pas le cas en faisant un update sur queryset # 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 # 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) .filter(checkout=ope.group.checkout)
.order_by('at') .order_by('at')
.last()) .last())
@ -1281,23 +1324,41 @@ def kpsul_cancel_operations(request):
# Note : si InventoryArticle est maj par .save(), stock_error # Note : si InventoryArticle est maj par .save(), stock_error
# est recalculé automatiquement # est recalculé automatiquement
if ope.article and ope.article_nb: if ope.article and ope.article_nb:
last_stock = (InventoryArticle.objects last_stock = (
InventoryArticle.objects
.select_related('inventory') .select_related('inventory')
.filter(article=ope.article) .filter(article=ope.article)
.order_by('inventory__at') .order_by('inventory__at')
.last()) .last()
)
if not last_stock or last_stock.inventory.at < ope.group.at: if not last_stock or last_stock.inventory.at < ope.group.at:
to_articles_stocks[ope.article] += ope.article_nb to_articles_stocks[ope.article] += ope.article_nb
if not opes: for transfer in transfers_all:
data['warnings']['already_canceled'] = opes_already_canceled 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) return JsonResponse(data)
negative_accounts = [] negative_accounts = []
# Checking permissions or stop # Checking permissions or stop
for account in to_accounts_balances: for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation( (perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account]) amount=to_accounts_balances[account])
required_perms |= perms required_perms |= perms
stop_all = stop_all or stop stop_all = stop_all or stop
if stop: if stop:
@ -1317,6 +1378,10 @@ def kpsul_cancel_operations(request):
with transaction.atomic(): with transaction.atomic():
(Operation.objects.filter(pk__in=opes) (Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=canceled_at)) .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: for account in to_accounts_balances:
( (
Account.objects Account.objects
@ -1329,20 +1394,22 @@ def kpsul_cancel_operations(request):
account.update_negative() account.update_negative()
for checkout in to_checkouts_balances: for checkout in to_checkouts_balances:
Checkout.objects.filter(pk=checkout.pk).update( Checkout.objects.filter(pk=checkout.pk).update(
balance = F('balance') + to_checkouts_balances[checkout]) balance=F('balance') + to_checkouts_balances[checkout])
for group in to_groups_amounts: for group in to_groups_amounts:
OperationGroup.objects.filter(pk=group.pk).update( OperationGroup.objects.filter(pk=group.pk).update(
amount = F('amount') + to_groups_amounts[group]) amount=F('amount') + to_groups_amounts[group])
for article in to_articles_stocks: for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update( Article.objects.filter(pk=article.pk).update(
stock = F('stock') + to_articles_stocks[article]) stock=F('stock') + to_articles_stocks[article])
# Websocket data # Websocket data
websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] } websocket_data = {'opegroups': [], 'opes': [],
'checkouts': [], 'articles': []}
# Need refresh from db cause we used update on querysets # Need refresh from db cause we used update on querysets
opegroups_pk = [ opegroup.pk for opegroup in to_groups_amounts ] opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts]
opegroups = (OperationGroup.objects 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: for opegroup in opegroups:
websocket_data['opegroups'].append({ websocket_data['opegroups'].append({
'cancellation': True, 'cancellation': True,
@ -1350,24 +1417,35 @@ def kpsul_cancel_operations(request):
'amount': opegroup['amount'], 'amount': opegroup['amount'],
'is_cof': opegroup['is_cof'], '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: for ope in opes:
websocket_data['opes'].append({ websocket_data['opes'].append({
'cancellation': True, 'cancellation': True,
'modelname': 'ope',
'id': ope, 'id': ope,
'canceled_by__trigramme': canceled_by__trigramme, 'canceled_by': canceled_by,
'canceled_at': canceled_at, '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 # Need refresh from db cause we used update on querysets
checkouts_pk = [ checkout.pk for checkout in to_checkouts_balances] checkouts_pk = [checkout.pk for checkout in to_checkouts_balances]
checkouts = (Checkout.objects checkouts = (Checkout.objects
.values('id', 'balance').filter(pk__in=checkouts_pk)) .values('id', 'balance')
.filter(pk__in=checkouts_pk))
for checkout in checkouts: for checkout in checkouts:
websocket_data['checkouts'].append({ websocket_data['checkouts'].append({
'id': checkout['id'], 'id': checkout['id'],
'balance': checkout['balance']}) 'balance': checkout['balance']})
# Need refresh from db cause we used update on querysets # Need refresh from db cause we used update on querysets
articles_pk = [ article.pk for articles in to_articles_stocks] articles_pk = [article.pk for articles in to_articles_stocks]
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk) articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
for article in articles: for article in articles:
websocket_data['articles'].append({ websocket_data['articles'].append({
@ -1375,83 +1453,169 @@ def kpsul_cancel_operations(request):
'stock': article['stock']}) 'stock': article['stock']})
consumers.KPsul.group_send('kfet.kpsul', websocket_data) consumers.KPsul.group_send('kfet.kpsul', websocket_data)
data['canceled'] = opes data['canceled']['opes'] = opes
if opes_already_canceled: data['canceled']['transfers'] = transfers
data['warnings']['already_canceled'] = opes_already_canceled if already_canceled:
data['warnings']['already_canceled'] = already_canceled
return JsonResponse(data) return JsonResponse(data)
@login_required @login_required
def history_json(request): def history_json(request):
# Récupération des paramètres # Récupération des paramètres
from_date = request.POST.get('from', None) from_date = request.GET.get('from', None)
to_date = request.POST.get('to', None) to_date = request.GET.get('to', None)
limit = request.POST.get('limit', None); checkouts = request.GET.getlist('checkouts[]', None)
checkouts = request.POST.getlist('checkouts[]', None) accounts = request.GET.getlist('accounts[]', None)
accounts = request.POST.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]
# Construction de la requête (sur les opérations) pour le prefetch # Construction de la requête (sur les opérations) pour le prefetch
queryset_prefetch = Operation.objects.select_related( ope_queryset_prefetch = Operation.objects.select_related(
'canceled_by__trigramme', 'addcost_for__trigramme', 'canceled_by', 'addcost_for',
'article__name') '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__id__in=accounts) |
Q(to_acc__id__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 # Construction de la requête principale
opegroups = (OperationGroup.objects opegroups = (
.prefetch_related(Prefetch('opes', queryset = queryset_prefetch)) OperationGroup.objects
.select_related('on_acc__trigramme', 'valid_by__trigramme') .prefetch_related(ope_prefetch)
.select_related('on_acc__trigramme',
'valid_by__trigramme')
.order_by('at') .order_by('at')
) )
transfergroups = (
TransferGroup.objects
.prefetch_related(transfer_prefetch)
.select_related('valid_by__trigramme')
.order_by('at')
)
# Application des filtres # Application des filtres
if from_date: if from_date:
opegroups = opegroups.filter(at__gte=from_date) opegroups = opegroups.filter(at__gte=from_date)
transfergroups = transfergroups.filter(at__gte=from_date)
if to_date: if to_date:
opegroups = opegroups.filter(at__lt=to_date) opegroups = opegroups.filter(at__lt=to_date)
transfergroups = transfergroups.filter(at__lt=to_date)
if checkouts: if checkouts:
opegroups = opegroups.filter(checkout_id__in=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: if accounts:
opegroups = opegroups.filter(on_acc_id__in=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]
# Construction de la réponse # Construction de la réponse
opegroups_list = [] related_data = defaultdict(list)
objects_data = defaultdict(list)
for opegroup in opegroups: for opegroup in opegroups:
opegroup_dict = { opegroup_dict = {
'id' : opegroup.id, 'id': opegroup.id,
'amount' : opegroup.amount, 'amount': opegroup.amount,
'at' : opegroup.at, 'at': opegroup.at,
'checkout_id': opegroup.checkout_id, 'is_cof': opegroup.is_cof,
'is_cof' : opegroup.is_cof, 'comment': opegroup.comment,
'comment' : opegroup.comment, 'trigramme':
'opes' : [],
'on_acc__trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None, opegroup.on_acc and opegroup.on_acc.trigramme or None,
} }
if request.user.has_perm('kfet.is_team'): 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) opegroup.valid_by and opegroup.valid_by.trigramme or None)
for ope in opegroup.opes.all(): for ope in opegroup.opes.all():
ope_dict = { ope_dict = {
'id' : ope.id, 'id': ope.id,
'type' : ope.type, 'amount': ope.amount,
'amount' : ope.amount, 'canceled_at': ope.canceled_at,
'article_nb' : ope.article_nb, 'is_cof': opegroup.is_cof,
'addcost_amount': ope.addcost_amount, 'trigramme':
'canceled_at' : ope.canceled_at, opegroup.on_acc and opegroup.on_acc.trigramme or None,
'article__name': 'opegroup__id': opegroup.id,
ope.article and ope.article.name or None,
'addcost_for__trigramme':
ope.addcost_for and ope.addcost_for.trigramme or None,
} }
if request.user.has_perm('kfet.is_team'): 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) ope.canceled_by and ope.canceled_by.trigramme or None)
opegroup_dict['opes'].append(ope_dict)
opegroups_list.append(opegroup_dict) if ope.type == Operation.PURCHASE:
return JsonResponse({ 'opegroups': opegroups_list }) 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 @teamkfet_required
@ -1461,26 +1625,32 @@ def kpsul_articles_data(request):
.filter(is_sold=True) .filter(is_sold=True)
.select_related('category')) .select_related('category'))
articlelist = [] articlelist = []
categorylist = []
# TODO: nice queryset, no duplicate categories
for article in articles: for article in articles:
articlelist.append({ articlelist.append({
'modelname': 'article',
'content': {
'id': article.id, 'id': article.id,
'name': article.name, 'name': article.name,
'price': article.price, 'price': article.price,
'stock': article.stock, 'stock': article.stock,
}, 'category__id': article.category.id,
'parent': { })
'modelname': 'category', categorylist.append({
'content': {
'id': article.category.id, 'id': article.category.id,
'name': article.category.name, 'name': article.category.name,
'has_addcost': article.category.has_addcost, 'has_addcost': article.category.has_addcost,
},
}
}) })
return JsonResponse(articlelist, safe=False)
data = {
'objects': {
'article': articlelist,
},
'related': {
'category': categorylist
}
}
return JsonResponse(data)
@ -1520,14 +1690,10 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
# Transfer views # Transfer views
# ----- # -----
@teamkfet_required @teamkfet_required
def transfers(request): def transfers(request):
transfergroups = (TransferGroup.objects return render(request, 'kfet/transfers.html')
.prefetch_related('transfers')
.order_by('-at'))
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
@teamkfet_required @teamkfet_required
def transfers_create(request): def transfers_create(request):
@ -1535,20 +1701,24 @@ def transfers_create(request):
return render(request, 'kfet/transfers_create.html', return render(request, 'kfet/transfers_create.html',
{ 'transfer_formset': transfer_formset }) { 'transfer_formset': transfer_formset })
@teamkfet_required @teamkfet_required
def perform_transfers(request): def perform_transfers(request):
data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 } data = {'errors': {}, 'transfers': [], 'transfergroup': 0}
# Checking transfer_formset # Checking transfer_formset
transfer_formset = TransferFormSet(request.POST) transfer_formset = TransferFormSet(request.POST)
if not transfer_formset.is_valid(): 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) transfers = transfer_formset.save(commit=False)
# Initializing vars # Initializing vars
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts required_perms = set(['kfet.add_transfer'])
# For balances of accounts
to_accounts_balances = defaultdict(lambda: 0)
for transfer in transfers: for transfer in transfers:
to_accounts_balances[transfer.from_acc] -= transfer.amount to_accounts_balances[transfer.from_acc] -= transfer.amount
@ -1560,7 +1730,7 @@ def perform_transfers(request):
# Checking if ok on all accounts # Checking if ok on all accounts
for account in to_accounts_balances: for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation( (perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account]) amount=to_accounts_balances[account])
required_perms |= perms required_perms |= perms
stop_all = stop_all or stop stop_all = stop_all or stop
if stop: if stop:
@ -1586,7 +1756,7 @@ def perform_transfers(request):
# Updating balances accounts # Updating balances accounts
for account in to_accounts_balances: for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update( Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account]) balance=F('balance') + to_accounts_balances[account])
account.refresh_from_db() account.refresh_from_db()
if account.balance < 0: if account.balance < 0:
if hasattr(account, 'negative'): if hasattr(account, 'negative'):
@ -1595,10 +1765,10 @@ def perform_transfers(request):
account.negative.save() account.negative.save()
else: else:
negative = AccountNegative( negative = AccountNegative(
account = account, start = timezone.now()) account=account, start=timezone.now())
negative.save() negative.save()
elif (hasattr(account, 'negative') elif (hasattr(account, 'negative') and
and not account.negative.balance_offset): not account.negative.balance_offset):
account.negative.delete() account.negative.delete()
# Saving transfer group # Saving transfer group
@ -1611,106 +1781,31 @@ def perform_transfers(request):
transfer.save() transfer.save()
data['transfers'].append(transfer.pk) data['transfers'].append(transfer.pk)
# Websocket data
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'modelname': 'transfergroup',
'id': transfergroup.pk,
'at': transfergroup.at,
'comment': transfergroup.comment,
'valid_by__trigramme': (transfergroup.valid_by and
transfergroup.valid_by.trigramme or None),
'opes': [],
}]
for transfer in transfers:
ope_data = {
'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,
}
websocket_data['opegroups'][0]['opes'].append(ope_data)
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
return JsonResponse(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)
print(required_perms)
print(request.user.get_all_permissions())
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): class InventoryList(ListView):
queryset = (Inventory.objects queryset = (Inventory.objects