Merge branch 'Aufinal/refactor_history' into 'aureplop/kpsul_js_refactor'

Aufinal/refactor history

See merge request !192
This commit is contained in:
Aurélien Delobelle 2017-05-16 10:48:07 +02:00
commit 311e0c48bd
11 changed files with 1542 additions and 841 deletions

View file

@ -40,6 +40,11 @@
width:90px;
}
#history .opegroup .infos {
text-align:center;
width:145px;
}
#history .opegroup .valid_by {
padding-left:20px
}
@ -67,6 +72,10 @@
text-align:right;
}
#history .ope .glyphicon {
padding-left:15px;
}
#history .ope .infos2 {
padding-left:15px;
}
@ -84,11 +93,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

@ -1,148 +1,215 @@
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>',
},
this.addOpeGroup = function(opegroup) {
var $day = this._getOrCreateDay(opegroup['at']);
var $opegroup = this._opeGroupHtml(opegroup);
'api_options': {
from: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss'),
},
$day.after($opegroup);
};
}
constructor(options) {
var all_options = $.extend({}, this.constructor.default_options, options);
this.api_options = all_options.api_options;
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._$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();
}
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 = '';
fetch(api_options) {
this.data.clear();
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;
$.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");
}
api_with_auth({
url: Urls['kfet.kpsul.cancel_operations'](),
data: to_cancel,
on_success: on_success,
})
}
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);
}
}
}
$ope_html
.data('ope', ope['id'])
.find('.amount').text(amount).end()
.find('.infos1').text(infos1).end()
.find('.infos2').text(infos2).end();
for (let opegroup of opegroups) {
if (opegroup['cancellation']) {
var update_data = { 'amount': opegroup.amount };
this.update_node('opegroup', opegroup.id, update_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 (opegroup['add'] && this.is_valid(opegroup)) {
this.add_node(opegroup);
}
}
if (ope['canceled_at'])
this.cancelOpe(ope, $ope_html);
return $ope_html;
var nb_opes = this._$container.find('.ope[canceled="false"]').length;
$('#nb_opes').text(nb_opes);
}
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
});
}
this.findOpe = function(id) {
return this.$container.find('.ope').filter(function() {
return $(this).data('ope') == id
});
}
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);
}
}
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,
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');
}
});
},
});
}
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) {
return Number.parseInt(v);
}
@ -340,11 +354,24 @@ function getErrorsHtml(data) {
return content;
}
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
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;
@ -357,7 +384,7 @@ function api_with_auth(settings, password) {
var on_success = settings.on_success || $.noop ;
var on_400 = settings.on_400 || $.noop ;
$.ajax({
return $.ajax({
dataType: "json",
url: url,
method: "POST",
@ -392,3 +419,9 @@ function api_with_auth(settings, password) {
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.checkout_manager = new CheckoutManager(this);
this.article_manager = new ArticleManager(this);
this.history = new KHistory({
api_options: {'opesonly': true},
});
this._init_events();
}
reset(soft) {
@ -23,6 +28,7 @@ class KPsulManager {
if (!soft) {
this.checkout_manager.reset();
this.article_manager.reset_data();
this.history.fetch();
}
}
@ -37,6 +43,14 @@ class KPsulManager {
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._$nb = $('#article_number');
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.list = new ArticleList() ;
this.autocomplete = new ArticleAutocomplete(this);
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() {
@ -429,7 +448,7 @@ class ArticleManager {
}
display_list() {
this.list.display(this._$container, this.templates) ;
this.display.render(this.data);
}
validate(article) {
@ -449,25 +468,18 @@ class ArticleManager {
}
reset_data() {
this._$container.html('');
this.list.clear();
this.list.fromAPI()
this.display.clear();
this.data.clear();
this.data.fromAPI()
.done( () => this.display_list() );
}
get_article(id) {
return this.list.find('article', id);
}
update_data(data) {
for (let article_dict of data.articles) {
var article = this.get_article(article_dict.id);
// For now, article additions are disregarded
if (article) {
article.stock = article_dict.stock;
this._$container.find('#article-'+article.id+' .stock')
.text(article.stock);
var updated = this.data.update('article', article_dict.id, article_dict);
if (updated) {
this.display.update(updated);
}
}
}
@ -496,7 +508,7 @@ class ArticleManager {
this._$container.on('click', '.article', function() {
var id = $(this).parent().attr('id').split('-')[1];
var article = that.list.find('article', id);
var article = that.data.find('article', id);
if (article)
that.validate(article);
});
@ -507,18 +519,26 @@ class ArticleManager {
that.reset();
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)
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();
@ -535,9 +555,9 @@ class ArticleManager {
class ArticleAutocomplete {
constructor(article_manager) {
constructor(article_manager, $container) {
this.manager = article_manager;
this._$container = article_manager._$container ;
this._$container = $container ;
this._$input = $('#article_autocomplete');
this.showAll() ;
@ -549,11 +569,12 @@ class ArticleAutocomplete {
// 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) || 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
if(e.keyCode == 8) {
that.update(text.substring(0, text.length-1), true);
@ -571,7 +592,7 @@ class ArticleAutocomplete {
update(prefix, backspace) {
this.resetMatch();
var article_list = this.manager.list ;
var article_list = this.manager.data ;
var lower = prefix.toLowerCase() ;
var that = this ;
@ -599,7 +620,7 @@ class ArticleAutocomplete {
updateDisplay() {
var that = this;
this.manager.list.traverse('category', function(category) {
this.manager.data.traverse('category', function(category) {
var is_active = false;
for (let article of category.articles) {
if (that.matching.indexOf(article) != -1) {
@ -634,7 +655,7 @@ class ArticleAutocomplete {
showAll() {
var that = this;
this.resetMatch();
this.manager.list.traverse('article', function(article) {
this.manager.data.traverse('article', function(article) {
that.matching.push(article);
});
this.updateDisplay();

View file

@ -4,10 +4,16 @@
{% load l10n %}
{% 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/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-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>
{% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
@ -94,33 +100,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

@ -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/multiple-select.css' %}">
<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>
@ -38,6 +39,9 @@
<div class="line"><b>Comptes</b> {{ filter_form.accounts }}</div>
</div>
</div>
<div class="buttons">
<a id="update_history" class="btn btn-primary btn-lg">Mettre à jour</a>
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
@ -51,8 +55,8 @@
<div class="content-right-block">
<h2>Opérations</h2>
<div>
<table id="history" class="table">
</table>
<div id="history">
</div>
</div>
</div>
</div>
@ -61,9 +65,9 @@
<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 = $('#from_date');
var $to_date = $('#to_date');
@ -78,7 +82,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');
@ -90,19 +96,8 @@ $(document).ready(function() {
var accounts = getSelectedMultiple($accounts);
data['accounts'] = accounts;
$.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);
}
$('#from_date').datetimepicker({
@ -117,7 +112,7 @@ $(document).ready(function() {
timeZone : 'Europe/Paris',
format : 'YYYY-MM-DD HH:mm',
stepping : 5,
defaultDate: moment(),
defaultDate: moment().add(5, 'minutes'), // workaround for 'stepping' rounding
locale : 'fr',
showTodayButton: true,
});
@ -133,69 +128,11 @@ $(document).ready(function() {
filter: true,
});
$("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 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();
Config.reset(updateHistory);
});
</script>

View file

@ -156,9 +156,6 @@
</div>
</div>
<div id="history2">
</div>
<form id="operationgroup_form" style="display:none;">{{ operationgroup_form }}</form>
<form id="operation_formset" style="display:none;">{{ operation_formset }}</form>
@ -199,21 +196,6 @@ $(document).ready(function() {
$('#id_comment').val('');
}
// -----
// Errors ajax
// -----
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
// -----
// Perform operations
// -----
@ -246,33 +228,6 @@ $(document).ready(function() {
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
// -----
@ -311,7 +266,7 @@ $(document).ready(function() {
var article_basket_html = $(item_basket_default_html);
article_basket_html
.attr('data-opeindex', index)
.find('.number').text(nb).end()
.find('.number').text('('+nb+'/'+article.stock+')').end()
.find('.name').text(article.name).end()
.find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof));
basket_container.prepend(article_basket_html);
@ -634,29 +589,6 @@ $(document).ready(function() {
.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');
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
// -----
@ -798,11 +667,10 @@ $(document).ready(function() {
coolReset(give_tri_focus);
kpsul.checkout_manager.reset();
resetPreviousOp();
khistory.reset();
Config.reset(function() {
kpsul.article_manager.reset_data();
displayAddcost();
getHistory();
kpsul.history.fetch();
});
}
@ -882,10 +750,23 @@ $(document).ready(function() {
updateBasketAmount: updateBasketAmount,
updateBasketRel: updateBasketRel,
performOperations: performOperations,
coolReset: coolReset,
};
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();
});

View file

@ -4,9 +4,15 @@
{% block extra_head %}
<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/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/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 %}
{% block title %}Transferts{% endblock %}
@ -18,6 +24,8 @@
<div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left">
<div class="content-left-top">
<div class="line line-big" id="nb_opes"></div>
<div class="line line-bigsub">transferts</div>
</div>
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.transfers.create' %}">
@ -31,23 +39,8 @@
<div class="content-right">
<div class="content-right-block">
<h2>Liste des transferts</h2>
<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 %}
</table>
<div id="history" class="table">
</div>
</div>
</div>
</div>
@ -56,88 +49,11 @@
<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

@ -203,8 +203,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

@ -18,7 +18,7 @@ from django.contrib.auth.models import User, Permission, Group
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.crypto import get_random_string
@ -1158,31 +1158,47 @@ def kpsul_perform_operations(request):
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'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
operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme,
'opes': [],
'modelname': 'opegroup',
'content': {
'id': operationgroup.pk,
'amount': operationgroup.amount,
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
'valid_by': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'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'] = [{
@ -1205,37 +1221,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 ]
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
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
opes = [] # Pas 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
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
@ -1262,10 +1304,11 @@ 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
.filter(checkout=ope.group.checkout)
.order_by('at')
.last())
last_statement = \
(CheckoutStatement.objects
.filter(checkout=ope.group.checkout)
.order_by('at')
.last())
if not last_statement or last_statement.at < ope.group.at:
if ope.is_checkout:
if ope.group.on_acc.is_cash:
@ -1281,23 +1324,41 @@ 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 = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
amount=to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
@ -1317,6 +1378,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
@ -1329,20 +1394,22 @@ def kpsul_cancel_operations(request):
account.update_negative()
for checkout in to_checkouts_balances:
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:
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:
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 = { '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_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,
@ -1350,24 +1417,35 @@ 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_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'],
'balance': checkout['balance']})
# 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)
for article in articles:
websocket_data['articles'].append({
@ -1375,83 +1453,169 @@ 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]
# Construction de la requête (sur les opérations) pour le prefetch
queryset_prefetch = Operation.objects.select_related(
'canceled_by__trigramme', 'addcost_for__trigramme',
'article__name')
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__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
opegroups = (OperationGroup.objects
.prefetch_related(Prefetch('opes', queryset = queryset_prefetch))
.select_related('on_acc__trigramme', 'valid_by__trigramme')
.order_by('at')
opegroups = (
OperationGroup.objects
.prefetch_related(ope_prefetch)
.select_related('on_acc__trigramme',
'valid_by__trigramme')
.order_by('at')
)
transfergroups = (
TransferGroup.objects
.prefetch_related(transfer_prefetch)
.select_related('valid_by__trigramme')
.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]
# 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':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
'id': opegroup.id,
'amount': opegroup.amount,
'at': opegroup.at,
'is_cof': opegroup.is_cof,
'comment': opegroup.comment,
'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,
'id': ope.id,
'amount': ope.amount,
'canceled_at': ope.canceled_at,
'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
@ -1461,26 +1625,32 @@ def kpsul_articles_data(request):
.filter(is_sold=True)
.select_related('category'))
articlelist = []
categorylist = []
# TODO: nice queryset, no duplicate categories
for article in articles:
articlelist.append({
'modelname': 'article',
'content': {
'id': article.id,
'name': article.name,
'price': article.price,
'stock': article.stock,
},
'parent': {
'modelname': 'category',
'content': {
'id': article.category.id,
'name': article.category.name,
'has_addcost': article.category.has_addcost,
},
}
'id': article.id,
'name': article.name,
'price': article.price,
'stock': article.stock,
'category__id': article.category.id,
})
return JsonResponse(articlelist, safe=False)
categorylist.append({
'id': article.category.id,
'name': article.category.name,
'has_addcost': article.category.has_addcost,
})
data = {
'objects': {
'article': articlelist,
},
'related': {
'category': categorylist
}
}
return JsonResponse(data)
@ -1520,14 +1690,10 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
# Transfer views
# -----
@teamkfet_required
def transfers(request):
transfergroups = (TransferGroup.objects
.prefetch_related('transfers')
.order_by('-at'))
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
return render(request, 'kfet/transfers.html')
@teamkfet_required
def transfers_create(request):
@ -1535,20 +1701,24 @@ 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 }
data = {'errors': {}, 'transfers': [], 'transfergroup': 0}
# 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)
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
@ -1560,7 +1730,7 @@ def perform_transfers(request):
# Checking if ok on all accounts
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
amount=to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
@ -1586,7 +1756,7 @@ def perform_transfers(request):
# Updating balances accounts
for account in to_accounts_balances:
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()
if account.balance < 0:
if hasattr(account, 'negative'):
@ -1595,10 +1765,10 @@ def perform_transfers(request):
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
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
@ -1611,106 +1781,31 @@ def perform_transfers(request):
transfer.save()
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)
@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):
queryset = (Inventory.objects