diff --git a/kfet/static/kfet/css/history.css b/kfet/static/kfet/css/history.css index 9cd4cd28..42e73527 100644 --- a/kfet/static/kfet/css/history.css +++ b/kfet/static/kfet/css/history.css @@ -20,7 +20,7 @@ z-index:10; } -#history .opegroup { +#history .group { height:30px; line-height:30px; background-color: #c63b52; @@ -30,29 +30,29 @@ overflow:auto; } -#history .opegroup .time { +#history .group .time { width:70px; } -#history .opegroup .trigramme { +#history .group .trigramme { width:55px; text-align:right; } -#history .opegroup .amount { +#history .group .amount { text-align:right; width:90px; } -#history .opegroup .valid_by { +#history .group .valid_by { padding-left:20px } -#history .opegroup .comment { +#history .group .comment { padding-left:20px; } -#history .ope { +#history .entry { position:relative; height:25px; line-height:24px; @@ -61,38 +61,38 @@ overflow:auto; } -#history .ope .amount { +#history .entry .amount { width:50px; text-align:right; } -#history .ope .infos1 { +#history .entry .infos1 { width:80px; text-align:right; } -#history .ope .infos2 { +#history .entry .infos2 { padding-left:15px; } -#history .ope .addcost { +#history .entry .addcost { padding-left:20px; } -#history .ope .canceled { +#history .entry .canceled { padding-left:20px; } -#history div.ope.ui-selected, #history div.ope.ui-selecting { +#history div.entry.ui-selected, #history div.entry.ui-selecting { background-color:rgba(200,16,46,0.6); color:#FFF; } -#history .ope.canceled, #history .transfer.canceled { +#history .entry.canceled { color:#444; } -#history .ope.canceled::before, #history.transfer.canceled::before { +#history .entry.canceled::before { position: absolute; content: ' '; width:100%; @@ -101,10 +101,11 @@ border-top: 1px solid rgba(200,16,46,0.5); } -#history .transfer .amount { - width:80px; +#history .group .infos { + text-align:center; + width:145px; } -#history .transfer .from_acc { - padding-left:10px; +#history .entry .glyphicon { + padding-left:15px; } diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index a7372b87..540c8239 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -2,31 +2,59 @@ function dateUTCToParis(date) { return moment.tz(date, 'UTC').tz('Europe/Paris'); } +// TODO : classifier (later) function KHistory(options = {}) { $.extend(this, KHistory.default_options, options); this.$container = $(this.container); + this.$container.selectable({ + filter: 'div.group, div.entry', + selected: function (e, ui) { + $(ui.selected).each(function () { + if ($(this).hasClass('group')) { + var id = $(this).data('id'); + $(this).siblings('.entry').filter(function () { + return $(this).data('group_id') == id + }).addClass('ui-selected'); + } + }); + }, + }); + this.reset = function () { this.$container.html(''); }; - this.addOpeGroup = function (opegroup) { - var $day = this._getOrCreateDay(opegroup['at']); - var $opegroup = this._opeGroupHtml(opegroup); + this.add_history_group = function (group) { + var $day = this._get_or_create_day(group['at']); + var $group = this._group_html(group); - $day.after($opegroup); + $day.after($group); - 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); + var trigramme = group['on_acc_trigramme']; + var is_cof = group['is_cof']; + var type = group['type'] + // TODO : simplifier ça ? + switch (type) { + case 'operation': + for (let ope of group['entries']) { + var $ope = this._ope_html(ope, is_cof, trigramme); + $ope.data('group_id', group['id']); + $group.after($ope); + } + break; + case 'transfer': + for (let transfer of group['entries']) { + var $transfer = this._transfer_html(transfer); + $transfer.data('group_id', group['id']); + $group.after($transfer); + } + break; } } - this._opeHtml = function (ope, is_cof, trigramme) { + this._ope_html = 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); @@ -54,7 +82,8 @@ function KHistory(options = {}) { } $ope_html - .data('ope', ope['id']) + .data('type', 'operation') + .data('id', ope['id']) .find('.amount').text(amount).end() .find('.infos1').text(infos1).end() .find('.infos2').text(infos2).end(); @@ -62,54 +91,89 @@ function KHistory(options = {}) { 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 + ')'); + $ope_html.find('.addcost').text('(' + amountDisplay(addcost_amount, is_cof) + ' UKF pour ' + addcost_for + ')'); } if (ope['canceled_at']) - this.cancelOpe(ope, $ope_html); + this.cancel_entry(ope, $ope_html); return $ope_html; } - this.cancelOpe = function (ope, $ope = null) { - if (!$ope) - $ope = this.findOpe(ope['id']); + this._transfer_html = function (transfer) { + var $transfer_html = $(this.template_transfer); + var parsed_amount = parseFloat(transfer['amount']); + var amount = parsed_amount.toFixed(2) + '€'; - 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'); + $transfer_html + .data('type', 'transfer') + .data('id', transfer['id']) + .find('.amount').text(amount).end() + .find('.infos1').text(transfer['from_acc']).end() + .find('.infos2').text(transfer['to_acc']).end(); - $ope.addClass('canceled').find('.canceled').text(cancel); + if (transfer['canceled_at']) + this.cancel_entry(transfer, $transfer_html); + + return $transfer_html; } - 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'] || ''; + this.cancel_entry = function (entry, $entry = null) { + if (!$entry) + $entry = this.find_entry(entry["id"], entry["type"]); - $opegroup_html - .data('opegroup', opegroup['id']) + var cancel = 'Annulé'; + var canceled_at = dateUTCToParis(entry['canceled_at']); + if (entry['canceled_by__trigramme']) + cancel += ' par ' + entry['canceled_by__trigramme']; + cancel += ' le ' + canceled_at.format('DD/MM/YY à HH:mm:ss'); + + $entry.addClass('canceled').find('.canceled').text(cancel); + } + + this._group_html = function (group) { + var type = group['type']; + + + switch (type) { + case 'operation': + var $group_html = $(this.template_opegroup); + var trigramme = group['on_acc__trigramme']; + var amount = amountDisplay( + parseFloat(group['amount']), group['is_cof'], trigramme); + break; + case 'transfer': + var $group_html = $(this.template_transfergroup); + $group_html.find('.infos').text('Transferts').end() + var trigramme = ''; + var amount = ''; + break; + } + + + var at = dateUTCToParis(group['at']).format('HH:mm:ss'); + var comment = group['comment'] || ''; + + $group_html + .data('type', type) + .data('id', group['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(); + $group_html.find('.trigramme').remove(); + $group_html.find('.info').remove(); - if (opegroup['valid_by__trigramme']) - $opegroup_html.find('.valid_by').text('Par ' + opegroup['valid_by__trigramme']); + if (group['valid_by__trigramme']) + $group_html.find('.valid_by').text('Par ' + group['valid_by__trigramme']); - return $opegroup_html; + return $group_html; } - this._getOrCreateDay = function (date) { + this._get_or_create_day = function (date) { var at = dateUTCToParis(date); var at_ser = at.format('YYYY-MM-DD'); var $day = this.$container.find('.day').filter(function () { @@ -118,35 +182,123 @@ function KHistory(options = {}) { 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')); + return $day.data('date', at_ser).text(at.format('D MMMM YYYY')); } - this.findOpeGroup = function (id) { - return this.$container.find('.opegroup').filter(function () { - return $(this).data('opegroup') == id + this.find_group = function (id, type = "operation") { + return this.$container.find('.group').filter(function () { + return ($(this).data('id') == id && $(this).data("type") == type) }); } - this.findOpe = function (id) { - return this.$container.find('.ope').filter(function () { - return $(this).data('ope') == id + this.find_entry = function (id, type = 'operation') { + return this.$container.find('.entry').filter(function () { + return ($(this).data('id') == id && $(this).data('type') == type) }); } - this.cancelOpeGroup = function (opegroup) { - var $opegroup = this.findOpeGroup(opegroup['id']); - var trigramme = $opegroup.find('.trigramme').text(); + this.update_opegroup = function (group, type = "operation") { + var $group = this.find_group(group['id'], type); + var trigramme = $group.find('.trigramme').text(); var amount = amountDisplay( - parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); - $opegroup.find('.amount').text(amount); + parseFloat(group['amount']), group['is_cof'], trigramme); + $group.find('.amount').text(amount); } + this.fetch = function (fetch_options) { + options = $.extend({}, this.fetch_options, fetch_options); + var that = this; + return $.ajax({ + dataType: "json", + url: django_urls["kfet.history.json"](), + method: "POST", + data: options, + }).done(function (data) { + for (let group of data['groups']) { + that.add_history_group(group); + } + }); + } + + this._cancel = function (type, opes, password = "") { + if (window.lock == 1) + return false + window.lock = 1; + var that = this; + return $.ajax({ + dataType: "json", + url: django_urls[`kfet.${type}s.cancel`](), + method: "POST", + data: opes, + beforeSend: function ($xhr) { + $xhr.setRequestHeader("X-CSRFToken", csrftoken); + if (password != '') + $xhr.setRequestHeader("KFetPassword", password); + }, + + }).done(function (data) { + window.lock = 0; + that.$container.find('.ui-selected').removeClass('ui-selected'); + for (let entry of data["canceled"]) { + entry["type"] = type; + that.cancel_entry(entry); + } + if (type == "operation") { + for (let opegroup of data["opegroups_to_update"]) { + that.update_opegroup(opegroup) + } + } + }).fail(function ($xhr) { + var data = $xhr.responseJSON; + switch ($xhr.status) { + case 403: + requestAuth(data, function (password) { + this.cancel(opes, password); + }); + break; + case 400: + displayErrors(getErrorsHtml(data)); + break; + } + window.lock = 0; + }); + } + + this.cancel_selected = function () { + var opes_to_cancel = { + "transfers": [], + "operations": [], + } + this.$container.find('.entry.ui-selected').each(function () { + type = $(this).data("type"); + opes_to_cancel[`${type}s`].push($(this).data("id")); + }); + if (opes_to_cancel["transfers"].length > 0 && opes_to_cancel["operations"].length > 0) { + // Lancer 2 requêtes AJAX et gérer tous les cas d'erreurs possibles est trop complexe + $.alert({ + title: 'Erreur', + content: "Impossible de supprimer des transferts et des opérations en même temps !", + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + }); + } else if (opes_to_cancel["transfers"].length > 0) { + delete opes_to_cancel["operations"]; + this._cancel("transfer", opes_to_cancel); + } else if (opes_to_cancel["operations"].length > 0) { + delete opes_to_cancel["transfers"]; + this._cancel("operation", opes_to_cancel); + } + } } KHistory.default_options = { container: '#history', template_day: '
', - template_opegroup: '
', - template_ope: '
', + template_opegroup: '
', + template_transfergroup: '
', + template_ope: '
', + template_transfer: '
', display_trigramme: true, } diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index bbd1cff7..d1af035a 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -5,6 +5,7 @@ {% block extra_head %} + @@ -81,7 +82,7 @@ $(document).ready(function() { {% endif %} -
+
@@ -93,29 +94,22 @@ $(document).ready(function() { khistory = new KHistory({ display_trigramme: false, - }); - - function getHistory() { - var data = { + fetch_options: { 'accounts': [{{ account.pk }}], } + }); - $.ajax({ - dataType: "json", - url : "{% url 'kfet.history.json' %}", - method : "POST", - data : data, - }) - .done(function(data) { - for (var i=0; i diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index ae63358e..94bba48c 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -5,6 +5,7 @@ {{ filter_form.media }} + {% endblock %} @@ -27,6 +28,9 @@
  • Comptes {{ filter_form.accounts }}
  • +
    + +
    {% endblock %} @@ -40,6 +44,8 @@ $(document).ready(function() { settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})} + window.lock = 0; + khistory = new KHistory(); var $from_date = $('#id_from_date'); @@ -67,17 +73,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 0) - confirmCancel(opes_to_cancel); + khistory.cancel_selected() } }); - - function confirmCancel(opes_to_cancel) { - var nb = opes_to_cancel.length; - var content = nb+" opérations vont être annulées"; - $.confirm({ - title: 'Confirmation', - content: content, - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - confirm: function() { - cancelOperations(opes_to_cancel); - } - }); - } - - function requestAuth(data, callback) { - var content = getErrorsHtml(data); - content += '', - $.confirm({ - title: 'Authentification requise', - content: content, - backgroundDismiss: true, - animation:'top', - closeAnimation:'bottom', - keyboardEnabled: true, - confirm: function() { - var password = this.$content.find('input').val(); - callback(password); - }, - onOpen: function() { - var that = this; - this.$content.find('input').on('keypress', function(e) { - if (e.keyCode == 13) - that.$confirmButton.click(); - }); - }, - }); - } - - function getErrorsHtml(data) { - var content = ''; - if ('missing_perms' in data['errors']) { - content += 'Permissions manquantes'; - content += ''; - } - if ('negative' in data['errors']) { - var url_base = "{% url 'kfet.account.update' LIQ}"; - url_base = base_url(0, url_base.length-8); - for (var i=0; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; - } - } - return content; - } - - function cancelOperations(opes_array, password = '') { - var data = { 'operations' : opes_array } - $.ajax({ - dataType: "json", - url : "{% url 'kfet.kpsul.cancel_operations' %}", - method : "POST", - data : data, - beforeSend: function ($xhr) { - $xhr.setRequestHeader("X-CSRFToken", csrftoken); - if (password != '') - $xhr.setRequestHeader("KFetPassword", password); - }, - - }) - .done(function(data) { - khistory.$container.find('.ui-selected').removeClass('ui-selected'); - }) - .fail(function($xhr) { - var data = $xhr.responseJSON; - switch ($xhr.status) { - case 403: - requestAuth(data, function(password) { - cancelOperations(opes_array, password); - }); - break; - case 400: - displayErrors(getErrorsHtml(data)); - break; - } - - }); - } - - getHistory(); }); diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 171c7030..7b292087 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -189,7 +189,7 @@ $(document).ready(function() { // ----- // Lock to avoid multiple requests - lock = 0; + window.lock = 0; // Retrieve settings @@ -479,9 +479,9 @@ $(document).ready(function() { var operations = $('#operation_formset'); function performOperations(password = '') { - if (lock == 1) + if (window.lock == 1) return false; - lock = 1; + window.lock = 1; var data = operationGroup.serialize() + '&' + operations.serialize(); $.ajax({ dataType: "json", @@ -497,7 +497,7 @@ $(document).ready(function() { .done(function(data) { updatePreviousOp(); coolReset(); - lock = 0; + window.lock = 0; }) .fail(function($xhr) { var data = $xhr.responseJSON; @@ -513,7 +513,7 @@ $(document).ready(function() { } break; } - lock = 0; + window.lock = 0; }); } @@ -522,55 +522,6 @@ $(document).ready(function() { performOperations(); }); - // ----- - // Cancel operations - // ----- - - var cancelButton = $('#cancel_operations'); - var cancelForm = $('#cancel_form'); - - function cancelOperations(opes_array, password = '') { - if (lock == 1) - return false - lock = 1; - var data = { 'operations' : opes_array } - $.ajax({ - dataType: "json", - url : "{% url 'kfet.kpsul.cancel_operations' %}", - method : "POST", - data : data, - beforeSend: function ($xhr) { - $xhr.setRequestHeader("X-CSRFToken", csrftoken); - if (password != '') - $xhr.setRequestHeader("KFetPassword", password); - }, - - }) - .done(function(data) { - coolReset(); - lock = 0; - }) - .fail(function($xhr) { - var data = $xhr.responseJSON; - switch ($xhr.status) { - case 403: - requestAuth(data, function(password) { - cancelOperations(opes_array, password); - }, triInput); - break; - case 400: - displayErrors(getErrorsHtml(data)); - break; - } - lock = 0; - }); - } - - // Event listeners - cancelButton.on('click', function() { - cancelOperations(); - }); - // ----- // Articles data // ----- @@ -1189,24 +1140,12 @@ $(document).ready(function() { // History // ----- - khistory = new KHistory(); - - function getHistory() { - var data = { + khistory = new KHistory({ + fetch_options: { 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 0) - cancelOperations(opes_to_cancel); + khistory.cancel_selected() } }); @@ -1333,16 +1253,9 @@ $(document).ready(function() { // ----- OperationWebSocket.add_handler(function(data) { - for (var i=0; i + + +{% endblock %} + {% block fixed %}
    @@ -16,109 +23,31 @@ {% block main %} -
    - {% for transfergroup in transfergroups %} -
    - {{ transfergroup.at }} - {{ transfergroup.valid_by.trigramme }} - {{ transfergroup.comment }} -
    - {% for transfer in transfergroup.transfers.all %} -
    - {{ transfer.amount }} € - {{ transfer.from_acc.trigramme }} - - {{ transfer.to_acc.trigramme }} -
    - {% endfor %} - {% endfor %} -
    + +
    diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 0a5c4e49..08d2cb32 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from decimal import Decimal from unittest import mock -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.test import Client, TestCase from django.urls import reverse from django.utils import timezone @@ -1997,9 +1997,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_once_with( "kfet.kpsul", { - "opegroups": [ + "groups": [ { "add": True, + "type": "operation", "at": mock.ANY, "amount": Decimal("-5.00"), "checkout__name": "Checkout", @@ -2008,7 +2009,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": None, - "opes": [ + "entries": [ { "id": operation.pk, "addcost_amount": None, @@ -2269,9 +2270,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_once_with( "kfet.kpsul", { - "opegroups": [ + "groups": [ { "add": True, + "type": "operation", "at": mock.ANY, "amount": Decimal("10.75"), "checkout__name": "Checkout", @@ -2280,7 +2282,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": "100", - "opes": [ + "entries": [ { "id": operation.pk, "addcost_amount": None, @@ -2443,9 +2445,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_once_with( "kfet.kpsul", { - "opegroups": [ + "groups": [ { "add": True, + "type": "operation", "at": mock.ANY, "amount": Decimal("-10.75"), "checkout__name": "Checkout", @@ -2454,7 +2457,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": None, - "opes": [ + "entries": [ { "id": operation.pk, "addcost_amount": None, @@ -2601,9 +2604,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_once_with( "kfet.kpsul", { - "opegroups": [ + "groups": [ { "add": True, + "type": "operation", "at": mock.ANY, "amount": Decimal("10.75"), "checkout__name": "Checkout", @@ -2612,7 +2616,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": "100", - "opes": [ + "entries": [ { "id": operation.pk, "addcost_amount": None, @@ -2712,9 +2716,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ - 0 - ]["opes"][0] + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ + "entries" + ][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2752,9 +2756,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ - 0 - ]["opes"][0] + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ + "entries" + ][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2790,9 +2794,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("106.00")) - ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ - 0 - ]["opes"][0] + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ + "entries" + ][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2826,9 +2830,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].refresh_from_db() self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00")) - ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ - 0 - ]["opes"][0] + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ + "entries" + ][0] self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -2861,9 +2865,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].refresh_from_db() self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00")) - ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ - 0 - ]["opes"][0] + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ + "entries" + ][0] self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -3170,9 +3174,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_once_with( "kfet.kpsul", { - "opegroups": [ + "groups": [ { "add": True, + "type": "operation", "at": mock.ANY, "amount": Decimal("-9.00"), "checkout__name": "Checkout", @@ -3181,7 +3186,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": None, - "opes": [ + "entries": [ { "id": operation_list[0].pk, "addcost_amount": None, @@ -3234,7 +3239,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): """ - url_name = "kfet.kpsul.cancel_operations" + url_name = "kfet.operations.cancel" url_expected = "/k-fet/k-psul/cancel_operations" http_methods = ["POST"] @@ -3353,7 +3358,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + json_data, + { + "canceled": [ + { + "id": operation.id, + # l'encodage des dates en JSON est relou... + "canceled_at": mock.ANY, + "canceled_by__trigramme": None, + } + ], + "errors": {}, + "warnings": {}, + "opegroups_to_update": [ + { + "id": group.pk, + "amount": str(group.amount), + "is_cof": group.is_cof, + } + ], + }, ) self.account.refresh_from_db() @@ -3365,26 +3389,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_with( "kfet.kpsul", - { - "opegroups": [ - { - "cancellation": True, - "id": group.pk, - "amount": Decimal("0.00"), - "is_cof": False, - } - ], - "opes": [ - { - "cancellation": True, - "id": operation.pk, - "canceled_by__trigramme": None, - "canceled_at": self.now + timedelta(seconds=15), - } - ], - "checkouts": [], - "articles": [{"id": self.article.pk, "stock": 22}], - }, + {"checkouts": [], "articles": [{"id": self.article.pk, "stock": 22}]}, ) def test_purchase_with_addcost(self): @@ -3541,7 +3546,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + json_data, + { + "canceled": [ + { + "id": operation.id, + # l'encodage des dates en JSON est relou... + "canceled_at": mock.ANY, + "canceled_by__trigramme": None, + } + ], + "errors": {}, + "warnings": {}, + "opegroups_to_update": [ + { + "id": group.pk, + "amount": str(group.amount), + "is_cof": group.is_cof, + } + ], + }, ) self.account.refresh_from_db() @@ -3554,22 +3578,6 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_with( "kfet.kpsul", { - "opegroups": [ - { - "cancellation": True, - "id": group.pk, - "amount": Decimal("0.00"), - "is_cof": False, - } - ], - "opes": [ - { - "cancellation": True, - "id": operation.pk, - "canceled_by__trigramme": None, - "canceled_at": self.now + timedelta(seconds=15), - } - ], "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}], "articles": [], }, @@ -3625,7 +3633,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + json_data, + { + "canceled": [ + { + "id": operation.id, + # l'encodage des dates en JSON est relou... + "canceled_at": mock.ANY, + "canceled_by__trigramme": None, + } + ], + "errors": {}, + "warnings": {}, + "opegroups_to_update": [ + { + "id": group.pk, + "amount": str(group.amount), + "is_cof": group.is_cof, + } + ], + }, ) self.account.refresh_from_db() @@ -3638,22 +3665,6 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.kpsul_consumer_mock.group_send.assert_called_with( "kfet.kpsul", { - "opegroups": [ - { - "cancellation": True, - "id": group.pk, - "amount": Decimal("0.00"), - "is_cof": False, - } - ], - "opes": [ - { - "cancellation": True, - "id": operation.pk, - "canceled_by__trigramme": None, - "canceled_at": self.now + timedelta(seconds=15), - } - ], "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}], "articles": [], }, @@ -3709,7 +3720,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + json_data, + { + "canceled": [ + { + "id": operation.id, + # l'encodage des dates en JSON est relou... + "canceled_at": mock.ANY, + "canceled_by__trigramme": None, + } + ], + "errors": {}, + "warnings": {}, + "opegroups_to_update": [ + { + "id": group.pk, + "amount": str(group.amount), + "is_cof": group.is_cof, + } + ], + }, ) self.account.refresh_from_db() @@ -3720,27 +3750,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.checkout.balance, Decimal("100.00")) self.kpsul_consumer_mock.group_send.assert_called_with( - "kfet.kpsul", - { - "opegroups": [ - { - "cancellation": True, - "id": group.pk, - "amount": Decimal("0.00"), - "is_cof": False, - } - ], - "opes": [ - { - "cancellation": True, - "id": operation.pk, - "canceled_by__trigramme": None, - "canceled_at": self.now + timedelta(seconds=15), - } - ], - "checkouts": [], - "articles": [], - }, + "kfet.kpsul", {"checkouts": [], "articles": []}, ) @mock.patch("django.utils.timezone.now") @@ -3961,13 +3971,33 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): group.refresh_from_db() self.assertEqual(group.amount, Decimal("10.75")) self.assertEqual(group.opes.exclude(canceled_at=None).count(), 3) - + self.maxDiff = None self.assertDictEqual( json_data, { - "canceled": [operation1.pk, operation2.pk], - "warnings": {"already_canceled": [operation3.pk]}, + "canceled": [ + { + "id": operation1.id, + # l'encodage des dates en JSON est relou... + "canceled_at": mock.ANY, + "canceled_by__trigramme": None, + }, + { + "id": operation2.id, + # l'encodage des dates en JSON est relou... + "canceled_at": mock.ANY, + "canceled_by__trigramme": None, + }, + ], "errors": {}, + "warnings": {"already_canceled": [operation3.pk]}, + "opegroups_to_update": [ + { + "id": group.pk, + "amount": str(group.amount), + "is_cof": group.is_cof, + } + ], }, ) @@ -4121,12 +4151,18 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): url_expected = "/k-fet/history.json" auth_user = "user" - auth_forbidden = [None] + auth_forbidden = [None, "noaccount"] def test_ok(self): r = self.client.post(self.url) self.assertEqual(r.status_code, 200) + def get_users_extra(self): + noaccount = User.objects.create(username="noaccount") + noaccount.set_password("noaccount") + noaccount.save() + return {"noaccount": noaccount} + class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.account.read.json" diff --git a/kfet/urls.py b/kfet/urls.py index 03c174f3..12c06d26 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -219,8 +219,8 @@ urlpatterns = [ ), path( "k-psul/cancel_operations", - views.kpsul_cancel_operations, - name="kfet.kpsul.cancel_operations", + views.cancel_operations, + name="kfet.operations.cancel", ), path( "k-psul/articles_data", @@ -252,7 +252,7 @@ urlpatterns = [ # ----- # Transfers urls # ----- - path("transfers/", views.transfers, name="kfet.transfers"), + path("transfers/", views.TransferView.as_view(), name="kfet.transfers"), path("transfers/new", views.transfers_create, name="kfet.transfers.create"), path("transfers/perform", views.perform_transfers, name="kfet.transfers.perform"), path("transfers/cancel", views.cancel_transfers, name="kfet.transfers.cancel"), diff --git a/kfet/views.py b/kfet/views.py index 655e856d..2d13b3d3 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -12,7 +12,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.models import Permission, User from django.contrib.messages.views import SuccessMessageMixin from django.db import transaction -from django.db.models import Count, F, Prefetch, Sum +from django.db.models import Count, F, Prefetch, Q, Sum from django.forms import formset_factory from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render @@ -1156,9 +1156,10 @@ def kpsul_perform_operations(request): # Websocket data websocket_data = {} - websocket_data["opegroups"] = [ + websocket_data["groups"] = [ { "add": True, + "type": "operation", "id": operationgroup.pk, "amount": operationgroup.amount, "checkout__name": operationgroup.checkout.name, @@ -1169,7 +1170,7 @@ def kpsul_perform_operations(request): operationgroup.valid_by and operationgroup.valid_by.trigramme or None ), "on_acc__trigramme": operationgroup.on_acc.trigramme, - "opes": [], + "entries": [], } ] for operation in operations: @@ -1187,7 +1188,7 @@ def kpsul_perform_operations(request): "canceled_by__trigramme": None, "canceled_at": None, } - websocket_data["opegroups"][0]["opes"].append(ope_data) + websocket_data["groups"][0]["entries"].append(ope_data) # Need refresh from db cause we used update on queryset operationgroup.checkout.refresh_from_db() websocket_data["checkouts"] = [ @@ -1207,7 +1208,7 @@ def kpsul_perform_operations(request): @teamkfet_required @kfet_password_auth -def kpsul_cancel_operations(request): +def cancel_operations(request): # Pour la réponse data = {"canceled": [], "warnings": {}, "errors": {}} @@ -1363,7 +1364,11 @@ def kpsul_cancel_operations(request): .filter(pk__in=opegroups_pk) .order_by("pk") ) - opes = sorted(opes) + opes = ( + Operation.objects.values("id", "canceled_at", "canceled_by__trigramme") + .filter(pk__in=opes) + .order_by("pk") + ) checkouts_pk = [checkout.pk for checkout in to_checkouts_balances] checkouts = ( Checkout.objects.values("id", "balance") @@ -1374,27 +1379,7 @@ def kpsul_cancel_operations(request): articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) # Websocket data - websocket_data = {"opegroups": [], "opes": [], "checkouts": [], "articles": []} - - for opegroup in opegroups: - websocket_data["opegroups"].append( - { - "cancellation": True, - "id": opegroup["id"], - "amount": opegroup["amount"], - "is_cof": opegroup["is_cof"], - } - ) - canceled_by__trigramme = canceled_by and canceled_by.trigramme or None - for ope in opes: - websocket_data["opes"].append( - { - "cancellation": True, - "id": ope, - "canceled_by__trigramme": canceled_by__trigramme, - "canceled_at": canceled_at, - } - ) + websocket_data = {"checkouts": [], "articles": []} for checkout in checkouts: websocket_data["checkouts"].append( {"id": checkout["id"], "balance": checkout["balance"]} @@ -1405,7 +1390,8 @@ def kpsul_cancel_operations(request): ) consumers.KPsul.group_send("kfet.kpsul", websocket_data) - data["canceled"] = opes + data["canceled"] = list(opes) + data["opegroups_to_update"] = list(opegroups) if opes_already_canceled: data["warnings"]["already_canceled"] = opes_already_canceled return JsonResponse(data) @@ -1416,49 +1402,86 @@ 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) + transfers_only = request.POST.get("transfersonly", False) + opes_only = request.POST.get("opesonly", False) + + # Construction de la requête (sur les transferts) pour le prefetch + + transfer_queryset_prefetch = Transfer.objects.select_related( + "from_acc", "to_acc", "canceled_by" + ) + + # Le check sur les comptes est dans le prefetch pour les transferts + if accounts: + transfer_queryset_prefetch = transfer_queryset_prefetch.filter( + Q(from_acc__in=accounts) | Q(to_acc__in=accounts) + ) + + if not request.user.has_perm("kfet.is_team"): + try: + acc = request.user.profile.account_kfet + transfer_queryset_prefetch = transfer_queryset_prefetch.filter( + Q(from_acc=acc) | Q(to_acc=acc) + ) + except Account.DoesNotExist: + return JsonResponse({}, status=403) + + transfer_prefetch = Prefetch( + "transfers", queryset=transfer_queryset_prefetch, to_attr="filtered_transfers" + ) # 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( "article", "canceled_by", "addcost_for" ) + ope_prefetch = Prefetch("opes", queryset=ope_queryset_prefetch) # Construction de la requête principale opegroups = ( - OperationGroup.objects.prefetch_related( - Prefetch("opes", queryset=queryset_prefetch) - ) + OperationGroup.objects.prefetch_related(ope_prefetch) .select_related("on_acc", "valid_by") .order_by("at") ) + transfergroups = ( + TransferGroup.objects.prefetch_related(transfer_prefetch) + .select_related("valid_by") + .order_by("at") + ) + # Application des filtres if from_date: opegroups = opegroups.filter(at__gte=from_date) + transfergroups = transfergroups.filter(at__gte=from_date) if to_date: opegroups = opegroups.filter(at__lt=to_date) + transfergroups = transfergroups.filter(at__lt=to_date) if checkouts: - opegroups = opegroups.filter(checkout_id__in=checkouts) + opegroups = opegroups.filter(checkout__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) + opegroups = opegroups.filter(on_acc__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 = [] + history_groups = [] for opegroup in opegroups: opegroup_dict = { + "type": "operation", "id": opegroup.id, "amount": opegroup.amount, "at": opegroup.at, "checkout_id": opegroup.checkout_id, "is_cof": opegroup.is_cof, "comment": opegroup.comment, - "opes": [], + "entries": [], "on_acc__trigramme": opegroup.on_acc and opegroup.on_acc.trigramme or None, } if request.user.has_perm("kfet.is_team"): @@ -1482,9 +1505,40 @@ def history_json(request): ope_dict["canceled_by__trigramme"] = ( 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}) + opegroup_dict["entries"].append(ope_dict) + history_groups.append(opegroup_dict) + for transfergroup in transfergroups: + if transfergroup.filtered_transfers: + transfergroup_dict = { + "type": "transfer", + "id": transfergroup.id, + "at": transfergroup.at, + "comment": transfergroup.comment, + "entries": [], + } + if request.user.has_perm("kfet.is_team"): + transfergroup_dict["valid_by__trigramme"] = ( + 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, + } + if request.user.has_perm("kfet.is_team"): + transfer_dict["canceled_by__trigramme"] = ( + transfer.canceled_by and transfer.canceled_by.trigramme or None + ) + transfergroup_dict["entries"].append(transfer_dict) + history_groups.append(transfergroup_dict) + + history_groups.sort(key=lambda group: group["at"]) + + return JsonResponse({"groups": history_groups}) @teamkfet_required @@ -1544,18 +1598,9 @@ config_update = permission_required("kfet.change_config")(SettingsUpdate.as_view # ----- -@teamkfet_required -def transfers(request): - transfers_pre = Prefetch( - "transfers", queryset=(Transfer.objects.select_related("from_acc", "to_acc")) - ) - - transfergroups = ( - TransferGroup.objects.select_related("valid_by") - .prefetch_related(transfers_pre) - .order_by("-at") - ) - return render(request, "kfet/transfers.html", {"transfergroups": transfergroups}) +@method_decorator(teamkfet_required, name="dispatch") +class TransferView(TemplateView): + template_name = "kfet/transfers.html" @teamkfet_required @@ -1746,7 +1791,12 @@ def cancel_transfers(request): elif hasattr(account, "negative") and not account.negative.balance_offset: account.negative.delete() - data["canceled"] = transfers + transfers = ( + Transfer.objects.values("id", "canceled_at", "canceled_by__trigramme") + .filter(pk__in=transfers) + .order_by("pk") + ) + data["canceled"] = list(transfers) if transfers_already_canceled: data["warnings"]["already_canceled"] = transfers_already_canceled return JsonResponse(data)