diff --git a/kfet/forms.py b/kfet/forms.py index e0d32102..5cc9d83f 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -14,7 +14,6 @@ from djconfig.forms import ConfigForm from gestioncof.models import CofProfile from kfet.models import ( Account, - AccountNegative, Article, ArticleCategory, Checkout, @@ -158,17 +157,6 @@ class UserInfoForm(UserForm): fields = ["first_name", "last_name"] -class AccountNegativeForm(forms.ModelForm): - class Meta: - model = AccountNegative - fields = [ - "authz_overdraft_amount", - "authz_overdraft_until", - "comment", - ] - widgets = {"authz_overdraft_until": DateTimeWidget()} - - # ----- # Checkout forms # ----- @@ -549,7 +537,7 @@ class TransferForm(forms.ModelForm): def clean_amount(self): amount = self.cleaned_data["amount"] if amount <= 0: - raise forms.ValidationError("Montant invalide") + raise forms.ValidationError("Le montant d'un transfert doit être positif") return amount class Meta: diff --git a/kfet/migrations/0078_negative_end.py b/kfet/migrations/0078_negative_end.py new file mode 100644 index 00000000..121a975e --- /dev/null +++ b/kfet/migrations/0078_negative_end.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.17 on 2021-02-28 01:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kfet", "0077_delete_frozen_permission"), + ] + + operations = [ + migrations.RemoveField( + model_name="accountnegative", + name="authz_overdraft_amount", + ), + migrations.RemoveField( + model_name="accountnegative", + name="authz_overdraft_until", + ), + migrations.RemoveField( + model_name="accountnegative", + name="comment", + ), + migrations.AddField( + model_name="accountnegative", + name="end", + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 628e5de6..887d3701 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -170,41 +170,23 @@ class Account(models.Model): return data def perms_to_perform_operation(self, amount): - overdraft_duration_max = kfet_config.overdraft_duration - overdraft_amount_max = kfet_config.overdraft_amount perms = set() - stop_ope = False # Checking is cash account if self.is_cash: # Yes, so no perms and no stop return set(), False + if self.need_comment: perms.add("kfet.perform_commented_operations") + new_balance = self.balance + amount + if new_balance < -kfet_config.overdraft_amount: + return set(), True + if new_balance < 0 and amount < 0: - # Retrieving overdraft amount limit - if ( - hasattr(self, "negative") - and self.negative.authz_overdraft_amount is not None - ): - overdraft_amount = -self.negative.authz_overdraft_amount - else: - overdraft_amount = -overdraft_amount_max - # Retrieving overdraft datetime limit - if ( - hasattr(self, "negative") - and self.negative.authz_overdraft_until is not None - ): - overdraft_until = self.negative.authz_overdraft_until - elif hasattr(self, "negative"): - overdraft_until = self.negative.start + overdraft_duration_max - else: - overdraft_until = timezone.now() + overdraft_duration_max - # Checking it doesn't break 1 rule - if new_balance < overdraft_amount or timezone.now() > overdraft_until: - stop_ope = True perms.add("kfet.perform_negative_operations") - return perms, stop_ope + + return perms, False # Surcharge Méthode save() avec gestions de User et CofProfile # Args: @@ -267,17 +249,26 @@ class Account(models.Model): def update_negative(self): if self.balance < 0: - if hasattr(self, "negative") and not self.negative.start: + # On met à jour le début de négatif seulement si la fin du négatif précédent + # est "vieille" + if ( + hasattr(self, "negative") + and self.negative.end is not None + and timezone.now() > self.negative.end + kfet_config.cancel_duration + ): self.negative.start = timezone.now() + self.negative.end = None self.negative.save() elif not hasattr(self, "negative"): self.negative = AccountNegative.objects.create( account=self, start=timezone.now() ) elif hasattr(self, "negative"): - # self.balance >= 0 - # TODO: méchanisme pour éviter de contourner le délai de négatif ? - self.negative.delete() + if self.negative.end is None: + self.negative.end = timezone.now() + elif timezone.now() > self.negative.end + kfet_config.cancel_duration: + # Idem: on supprime le négatif après une légère période + self.negative.delete() class UserHasAccount(Exception): def __init__(self, trigramme): @@ -302,26 +293,11 @@ class AccountNegative(models.Model): Account, on_delete=models.CASCADE, related_name="negative" ) start = models.DateTimeField(blank=True, null=True, default=None) - authz_overdraft_amount = models.DecimalField( - "négatif autorisé", - max_digits=6, - decimal_places=2, - blank=True, - null=True, - default=None, - ) - authz_overdraft_until = models.DateTimeField( - "expiration du négatif", blank=True, null=True, default=None - ) - comment = models.CharField("commentaire", max_length=255, blank=True) + end = models.DateTimeField(blank=True, null=True, default=None) class Meta: permissions = (("view_negs", "Voir la liste des négatifs"),) - @property - def until_default(self): - return self.start + kfet_config.overdraft_duration - class CheckoutQuerySet(models.QuerySet): def is_valid(self): diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 06b10d17..4c2a2664 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -257,11 +257,11 @@ function KHistory(options = {}) { switch ($xhr.status) { case 403: requestAuth(data, function (password) { - this.cancel(opes, password); + that._cancel(type, opes, password); }); break; case 400: - displayErrors(getErrorsHtml(data)); + displayErrors(data); break; } window.lock = 0; diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 2030304f..14c4bc40 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -106,116 +106,80 @@ function amountToUKF(amount, is_cof = false, account = false) { return rounding(amount * coef_cof * 10); } -function getErrorsHtml(data) { - var content = ''; - if (!data) - return "L'utilisateur n'est pas dans l'équipe"; - if ('operation_group' in data['errors']) { - content += 'Général'; - content += ''; +function getErrorsHtml(data, is_error = true) { + if (is_error) { + data = data.map(error => error.message) } - if ('missing_perms' in data['errors']) { - content += 'Permissions manquantes'; - content += ''; - } - if ('negative' in data['errors']) { - if (window.location.pathname.startsWith('/gestion/')) { - var url_base = '/gestion/k-fet/accounts/'; - } else { - var url_base = '/k-fet/accounts/'; - } - for (var i = 0; i < data['errors']['negative'].length; i++) { - content += 'Autorisation de négatif requise pour ' + data['errors']['negative'][i] + ''; - } - } - if ('addcost' in data['errors']) { - content += ''; - } - if ('account' in data['errors']) { - content += 'Général'; - content += ''; - } - if ('frozen' in data['errors']) { - content += 'Général'; - content += ''; + + var content = is_error ? "Général :" : "Permissions manquantes :"; + content += ""; + return content; } function requestAuth(data, callback, focus_next = null) { - 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; - var capslock = -1; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown - this.$content.find('input').on('keypress', function (e) { - if (e.keyCode == 13) - that.$confirmButton.click(); + var content = getErrorsHtml(data["missing_perms"], is_error = false); + content += '
'; - var s = String.fromCharCode(e.which); - if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey) || //caps on, shift off - (s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on - capslock = 1; - } else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey) || //caps off, shift off - (s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on - capslock = 0; - } - if (capslock == 1) - $('.capslock .glyphicon').show(); - else if (capslock == 0) - $('.capslock .glyphicon').hide(); - }); - // Capslock key is not detected by keypress - this.$content.find('input').on('keydown', function (e) { - if (e.which == 20) { - capslock = 1 - capslock; - } - if (capslock == 1) - $('.capslock .glyphicon').show(); - else if (capslock == 0) - $('.capslock .glyphicon').hide(); - }); - }, - onClose: function () { - if (focus_next) - this._lastFocused = focus_next; - } + $.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; + var capslock = -1; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown + this.$content.find('input').on('keypress', function (e) { + if (e.keyCode == 13) + that.$confirmButton.click(); - }); + var s = String.fromCharCode(e.which); + if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey) || //caps on, shift off + (s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on + capslock = 1; + } else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey) || //caps off, shift off + (s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on + capslock = 0; + } + if (capslock == 1) + $('.capslock .glyphicon').show(); + else if (capslock == 0) + $('.capslock .glyphicon').hide(); + }); + // Capslock key is not detected by keypress + this.$content.find('input').on('keydown', function (e) { + if (e.which == 20) { + capslock = 1 - capslock; + } + if (capslock == 1) + $('.capslock .glyphicon').show(); + else if (capslock == 0) + $('.capslock .glyphicon').hide(); + }); + }, + onClose: function () { + if (focus_next) + this._lastFocused = focus_next; + } + + }); } -function displayErrors(html) { +function displayErrors(data) { + const content = getErrorsHtml(data["errors"], is_error = true); $.alert({ title: 'Erreurs', - content: html, + content: content, backgroundDismiss: true, animation: 'top', closeAnimation: 'bottom', diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 9ca9cd99..c2390f6d 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -10,26 +10,12 @@ {{ negatives|length }} compte{{ negatives|length|pluralize }} en négatif
-
- Total: {{ negatives_sum|floatformat:2 }}€ -
-
- Plafond par défaut -
    -
  • Montant: {{ kfet_config.overdraft_amount }}€
  • -
  • Pendant: {{ kfet_config.overdraft_duration }}
  • -
+
+ {{ negatives_sum|floatformat:2 }}€ + de négatif total
-{% if perms.kfet.change_settings %} -
-
-
-
-{% endif %} - {% endblock %} {% block main %} @@ -43,8 +29,6 @@ Nom Balance Début - Découvert autorisé - Jusqu'au @@ -60,10 +44,6 @@ {{ neg.start|date:'d/m/Y H:i'}} - {{ neg.authz_overdraft_amount|default_if_none:'' }} - - {{ neg.authz_overdraft_until|date:'d/m/Y H:i' }} - {% endfor %} diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index 2bab6c1d..65965d83 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -35,23 +35,9 @@ Modification de mes informations {% include 'kfet/form_snippet.html' with form=frozen_form %} {% include 'kfet/form_snippet.html' with form=group_form %} {% include 'kfet/form_snippet.html' with form=pwd_form %} - {% include 'kfet/form_snippet.html' with form=negative_form %} - {% if perms.kfet.is_team %} + {% include 'kfet/form_authentication_snippet.html' %} - {% endif %} {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %} - - {% endblock %} \ No newline at end of file diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index ece98578..8259d694 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -376,10 +376,10 @@ $(document).ready(function() { requestAuth(data, performOperations, articleSelect); break; case 400: - if ('need_comment' in data['errors']) { + if ('need_comment' in data) { askComment(performOperations); } else { - displayErrors(getErrorsHtml(data)); + displayErrors(data); } break; } @@ -1074,7 +1074,7 @@ $(document).ready(function() { }, triInput); break; case 400: - askAddcost(getErrorsHtml(data)); + askAddcost(getErrorsHtml(data["errors"], is_error=true)); break; } }); diff --git a/kfet/templates/kfet/left_account.html b/kfet/templates/kfet/left_account.html index e1673d22..a68845ed 100644 --- a/kfet/templates/kfet/left_account.html +++ b/kfet/templates/kfet/left_account.html @@ -54,12 +54,6 @@ {% if account.negative.start %}
  • Depuis le {{ account.negative.start|date:"d/m/Y à H:i" }}
  • {% endif %} -
  • - Plafond : - {{ account.negative.authz_overdraft_amount|default:kfet_config.overdraft_amount }} € - jusqu'au - {{ account.negative.authz_overdraft_until|default:account.negative.until_default|date:"d/m/Y à H:i" }} -
  • {% endif %} diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index a4a1a450..fc429d97 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -72,7 +72,7 @@ $(document).ready(function () { var $next = $form.next('.transfer_form').find('.from_acc input'); } var $input_id = $input.next('input'); - if (isValidTrigramme(trigramme)) { + if (trigramme.is_valid_trigramme()) { getAccountData(trigramme, function(data) { $input_id.val(data.id); $data.text(data.name); @@ -122,7 +122,7 @@ $(document).ready(function () { requestAuth(data, performTransfers); break; case 400: - displayErrors(getErrorsHtml(data)); + displayErrors(data); break; } }); diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py index 7ce6605c..a534493d 100644 --- a/kfet/tests/test_models.py +++ b/kfet/tests/test_models.py @@ -1,10 +1,12 @@ -import datetime +from datetime import datetime, timedelta, timezone as tz +from decimal import Decimal +from unittest import mock from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from kfet.models import Account, Checkout +from kfet.models import Account, AccountNegative, Checkout from .utils import create_user @@ -28,6 +30,56 @@ class AccountTests(TestCase): with self.assertRaises(Account.DoesNotExist): Account.objects.get_by_password("bernard") + @mock.patch("django.utils.timezone.now") + def test_negative_creation(self, mock_now): + now = datetime(2005, 7, 15, tzinfo=tz.utc) + mock_now.return_value = now + self.account.balance = Decimal(-10) + self.account.update_negative() + + self.assertTrue(hasattr(self.account, "negative")) + self.assertEqual(self.account.negative.start, now) + + @mock.patch("django.utils.timezone.now") + def test_negative_no_reset(self, mock_now): + now = datetime(2005, 7, 15, tzinfo=tz.utc) + mock_now.return_value = now + + self.account.balance = Decimal(-10) + AccountNegative.objects.create( + account=self.account, start=now - timedelta(minutes=3) + ) + self.account.refresh_from_db() + + self.account.balance = Decimal(5) + self.account.update_negative() + self.assertTrue(hasattr(self.account, "negative")) + + self.account.balance = Decimal(-10) + self.account.update_negative() + self.assertEqual(self.account.negative.start, now - timedelta(minutes=3)) + + @mock.patch("django.utils.timezone.now") + def test_negative_eventually_resets(self, mock_now): + now = datetime(2005, 7, 15, tzinfo=tz.utc) + mock_now.return_value = now + + self.account.balance = Decimal(-10) + AccountNegative.objects.create( + account=self.account, start=now - timedelta(minutes=20) + ) + self.account.refresh_from_db() + self.account.balance = Decimal(5) + + mock_now.return_value = now - timedelta(minutes=10) + self.account.update_negative() + + mock_now.return_value = now + self.account.update_negative() + self.account.refresh_from_db() + + self.assertFalse(hasattr(self.account, "negative")) + class CheckoutTests(TestCase): def setUp(self): @@ -39,7 +91,7 @@ class CheckoutTests(TestCase): self.c = Checkout( created_by=self.u_acc, valid_from=self.now, - valid_to=self.now + datetime.timedelta(days=1), + valid_to=self.now + timedelta(days=1), ) def test_initial_statement(self): diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index c4d31ae2..7a7eddcb 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -15,7 +15,6 @@ from ..auth.utils import hash_password from ..config import kfet_config from ..models import ( Account, - AccountNegative, Article, ArticleCategory, Checkout, @@ -1856,7 +1855,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["operation_group"], ["on_acc"]) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_on_acc", "invalid_formset"], + ) def test_group_on_acc_expects_comment(self): user_add_perms(self.users["team"], ["kfet.perform_commented_operations"]) @@ -1899,7 +1901,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["need_comment"], True) + self.assertEqual(json_data["need_comment"], True) def test_invalid_group_on_acc_needs_comment_requires_perm(self): self.account.trigramme = "#13" @@ -1922,8 +1924,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], - ["[kfet] Enregistrer des commandes avec commentaires"], + json_data["missing_perms"], + ["Enregistrer des commandes avec commentaires"], ) def test_error_on_acc_frozen(self): @@ -1945,7 +1947,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["frozen"], [self.account.trigramme]) + self.assertEqual([e["code"] for e in json_data["errors"]], ["frozen_acc"]) def test_invalid_group_checkout(self): self.checkout.valid_from -= timedelta(days=300) @@ -1957,7 +1959,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["operation_group"], ["checkout"]) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_checkout", "invalid_formset"], + ) def test_invalid_group_expects_one_operation(self): data = dict(self.base_post_data) @@ -1965,7 +1970,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["operations"], []) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], + ) def test_purchase_with_user_is_nof_cof(self): self.account.cofprofile.is_cof = False @@ -2023,12 +2031,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check response content self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) # Check object updates @@ -2179,9 +2182,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], - [{"__all__": ["Un achat nécessite un article et une quantité"]}], + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_purchase_expects_article_nb(self): @@ -2199,9 +2202,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], - [{"__all__": ["Un achat nécessite un article et une quantité"]}], + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_purchase_expects_article_nb_greater_than_1(self): @@ -2219,16 +2222,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], - [ - { - "__all__": ["Un achat nécessite un article et une quantité"], - "article_nb": [ - "Assurez-vous que cette valeur est supérieure ou " "égale à 1." - ], - } - ], + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_operation_not_purchase_with_cash(self): @@ -2247,7 +2243,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["account"], "LIQ") + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_liq"], + ) def test_deposit(self): user_add_perms(self.users["team"], ["kfet.perform_deposit"]) @@ -2300,12 +2299,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) self.account.refresh_from_db() @@ -2364,8 +2358,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_deposit_too_many_params(self): @@ -2383,8 +2378,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_deposit_expects_positive_amount(self): @@ -2402,8 +2398,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Charge non positive"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_deposit_requires_perm(self): @@ -2421,9 +2418,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["missing_perms"], ["[kfet] Effectuer une charge"] - ) + self.assertEqual(json_data["missing_perms"], ["Effectuer une charge"]) def test_withdraw(self): data = dict( @@ -2475,12 +2470,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) self.account.refresh_from_db() @@ -2539,8 +2529,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_withdraw_too_many_params(self): @@ -2558,8 +2549,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_withdraw_expects_negative_amount(self): @@ -2577,8 +2569,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Retrait non négatif"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_edit(self): @@ -2634,12 +2627,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) self.account.refresh_from_db() @@ -2700,8 +2688,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], - ["[kfet] Modifier la balance d'un compte"], + json_data["missing_perms"], + ["Modifier la balance d'un compte"], ) def test_invalid_edit_expects_comment(self): @@ -2721,7 +2709,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["need_comment"], True) + self.assertEqual(json_data["need_comment"], True) def _setup_addcost(self): self.register_user("addcost", create_user("addcost", "ADD")) @@ -3008,62 +2996,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"], - {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, + json_data["missing_perms"], + ["Enregistrer des commandes en négatif"], ) - def test_invalid_negative_exceeds_allowed_duration_from_config(self): - user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) - kfet_config.set(overdraft_duration=timedelta(days=5)) - self.account.balance = Decimal("1.00") - self.account.save() - self.account.negative = AccountNegative.objects.create( - account=self.account, start=timezone.now() - timedelta(days=5, minutes=1) - ) - - data = dict( - self.base_post_data, - **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - } - ) - resp = self.client.post(self.url, data) - - self.assertEqual(resp.status_code, 403) - json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"negative": ["000"]}) - - def test_invalid_negative_exceeds_allowed_duration_from_account(self): - user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) - kfet_config.set(overdraft_duration=timedelta(days=5)) - self.account.balance = Decimal("1.00") - self.account.save() - self.account.negative = AccountNegative.objects.create( - account=self.account, - start=timezone.now() - timedelta(days=3), - authz_overdraft_until=timezone.now() - timedelta(seconds=1), - ) - - data = dict( - self.base_post_data, - **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - } - ) - resp = self.client.post(self.url, data) - - self.assertEqual(resp.status_code, 403) - json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"negative": ["000"]}) - def test_invalid_negative_exceeds_amount_allowed_from_config(self): user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) kfet_config.set(overdraft_amount=Decimal("-1.00")) @@ -3083,38 +3019,13 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 403) + self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"negative": ["000"]}) - - def test_invalid_negative_exceeds_amount_allowed_from_account(self): - user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) - kfet_config.set(overdraft_amount=Decimal("10.00")) - self.account.balance = Decimal("1.00") - self.account.save() - self.account.update_negative() - self.account.negative = AccountNegative.objects.create( - account=self.account, - start=timezone.now() - timedelta(days=3), - authz_overdraft_amount=Decimal("1.00"), + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["negative"], ) - data = dict( - self.base_post_data, - **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - } - ) - resp = self.client.post(self.url, data) - - self.assertEqual(resp.status_code, 403) - json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"negative": ["000"]}) - def test_multi_0(self): article2 = Article.objects.create( name="Article 2", @@ -3198,12 +3109,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check response content self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation_list[0].pk, operation_list[1].pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) # Check object updates @@ -3342,7 +3248,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {}) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_request"], + ) def test_invalid_operation_not_exist(self): data = {"operations[]": ["1000"]} @@ -3350,7 +3259,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"opes_notexisting": [1000]}) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["cancel_missing"], + ) @mock.patch("django.utils.timezone.now") def test_purchase(self, now_mock): @@ -3414,7 +3326,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3602,7 +3514,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3689,7 +3601,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3776,7 +3688,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3839,8 +3751,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"], - {"missing_perms": ["[kfet] Annuler des commandes non récentes"]}, + json_data["missing_perms"], + ["Annuler des commandes non récentes"], ) def test_already_canceled(self): @@ -3964,9 +3876,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): data = {"operations[]": [str(operation.pk)]} resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 403) + self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"negative": [self.account.trigramme]}) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["negative"], + ) def test_invalid_negative_requires_perms(self): kfet_config.set(overdraft_amount=Decimal("40.00")) @@ -3985,8 +3900,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"], - {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, + json_data["missing_perms"], + ["Enregistrer des commandes en négatif"], ) def test_partial_0(self): @@ -4036,7 +3951,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, }, ], - "errors": {}, + "errors": [], "warnings": {"already_canceled": [operation3.pk]}, "opegroups_to_update": [ { diff --git a/kfet/views.py b/kfet/views.py index 83bf380a..0d9f9544 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -15,7 +15,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import SuspiciousOperation, ValidationError from django.db import transaction from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum -from django.forms import formset_factory +from django.forms import ValidationError, formset_factory from django.http import ( Http404, HttpResponseBadRequest, @@ -39,7 +39,6 @@ from kfet.decorators import teamkfet_required from kfet.forms import ( AccountForm, AccountFrozenForm, - AccountNegativeForm, AccountNoTriForm, AccountPwdForm, AccountStatForm, @@ -355,11 +354,6 @@ def account_update(request, trigramme): frozen_form = AccountFrozenForm(request.POST, instance=account) pwd_form = AccountPwdForm() - if hasattr(account, "negative"): - negative_form = AccountNegativeForm(instance=account.negative) - else: - negative_form = None - if request.method == "POST": self_update = request.user == account.user account_form = AccountForm(request.POST, instance=account) @@ -381,14 +375,6 @@ def account_update(request, trigramme): elif group_form.has_changed(): warnings.append("statut d'équipe") - if hasattr(account, "negative"): - negative_form = AccountNegativeForm(request.POST, instance=account.negative) - - if request.user.has_perm("kfet.change_accountnegative"): - forms.append(negative_form) - elif negative_form.has_changed(): - warnings.append("négatifs") - # Il ne faut pas valider `pwd_form` si elle est inchangée if pwd_form.has_changed(): if self_update or request.user.has_perm("kfet.change_account_password"): @@ -437,7 +423,6 @@ def account_update(request, trigramme): "account_form": account_form, "frozen_form": frozen_form, "group_form": group_form, - "negative_form": negative_form, "pwd_form": pwd_form, }, ) @@ -482,9 +467,11 @@ class AccountDelete(PermissionRequiredMixin, DeleteView): class AccountNegativeList(ListView): - queryset = AccountNegative.objects.select_related( - "account", "account__cofprofile__user" - ).exclude(account__trigramme="#13") + queryset = ( + AccountNegative.objects.select_related("account", "account__cofprofile__user") + .filter(account__balance__lt=0) + .exclude(account__trigramme="#13") + ) template_name = "kfet/account_negative.html" context_object_name = "negatives" @@ -977,15 +964,18 @@ def kpsul_checkout_data(request): @kfet_password_auth def kpsul_update_addcost(request): addcost_form = AddcostForm(request.POST) + data = {"errors": []} if not addcost_form.is_valid(): - data = {"errors": {"addcost": list(addcost_form.errors)}} + for (field, errors) in addcost_form.errors.items(): + for error in errors: + data["errors"].append({"code": f"invalid_{field}", "message": error}) + return JsonResponse(data, status=400) + required_perms = ["kfet.manage_addcosts"] if not request.user.has_perms(required_perms): - data = { - "errors": {"missing_perms": get_missing_perms(required_perms, request.user)} - } + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) trigramme = addcost_form.cleaned_data["trigramme"] @@ -1000,14 +990,13 @@ def kpsul_update_addcost(request): def get_missing_perms(required_perms: List[str], user: User) -> List[str]: - def get_perm_description(app_label: str, codename: str) -> str: - name = Permission.objects.values_list("name", flat=True).get( + def get_perm_name(app_label: str, codename: str) -> str: + return Permission.objects.values_list("name", flat=True).get( codename=codename, content_type__app_label=app_label ) - return "[{}] {}".format(app_label, name) missing_perms = [ - get_perm_description(*perm.split(".")) + get_perm_name(*perm.split(".")) for perm in required_perms if not user.has_perm(perm) ] @@ -1019,17 +1008,31 @@ def get_missing_perms(required_perms: List[str], user: User) -> List[str]: @kfet_password_auth def kpsul_perform_operations(request): # Initializing response data - data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}} + data = {"errors": []} # Checking operationgroup operationgroup_form = KPsulOperationGroupForm(request.POST) if not operationgroup_form.is_valid(): - data["errors"]["operation_group"] = list(operationgroup_form.errors) + for field in operationgroup_form.errors: + verbose_field, feminin = ( + ("compte", "") if field == "on_acc" else ("caisse", "e") + ) + data["errors"].append( + { + "code": f"invalid_{field}", + "message": f"Pas de {verbose_field} sélectionné{feminin}", + } + ) # Checking operation_formset operation_formset = KPsulOperationFormSet(request.POST) if not operation_formset.is_valid(): - data["errors"]["operations"] = list(operation_formset.errors) + data["errors"].append( + { + "code": "invalid_formset", + "message": "Formulaire d'opérations vide ou invalide", + } + ) # Returning BAD REQUEST if errors if data["errors"]: @@ -1038,6 +1041,7 @@ def kpsul_perform_operations(request): # Pre-saving (no commit) operationgroup = operationgroup_form.save(commit=False) operations = operation_formset.save(commit=False) + on_acc = operationgroup.on_acc # Retrieving COF grant cof_grant = kfet_config.subvention_cof @@ -1051,13 +1055,13 @@ def kpsul_perform_operations(request): to_addcost_for_balance = 0 # For balance of addcost_for to_checkout_balance = 0 # For balance of selected checkout to_articles_stocks = defaultdict(lambda: 0) # For stocks articles - is_addcost = all( - (addcost_for, addcost_amount, addcost_for != operationgroup.on_acc) - ) - need_comment = operationgroup.on_acc.need_comment + is_addcost = all((addcost_for, addcost_amount, addcost_for != on_acc)) + need_comment = on_acc.need_comment - if operationgroup.on_acc.is_frozen: - data["errors"]["frozen"] = [operationgroup.on_acc.trigramme] + if on_acc.is_frozen: + data["errors"].append( + {"code": "frozen_acc", "message": f"Le compte {on_acc.trigramme} est gelé"} + ) # Filling data of each operations # + operationgroup + calculating other stuffs @@ -1069,19 +1073,23 @@ def kpsul_perform_operations(request): operation.addcost_amount = addcost_amount * operation.article_nb operation.amount -= operation.addcost_amount to_addcost_for_balance += operation.addcost_amount - if operationgroup.on_acc.is_cash: + if on_acc.is_cash: to_checkout_balance += -operation.amount - if ( - operationgroup.on_acc.is_cof - and operation.article.category.has_reduction - ): + if on_acc.is_cof and operation.article.category.has_reduction: if is_addcost and operation.article.category.has_addcost: operation.addcost_amount /= cof_grant_divisor operation.amount = operation.amount / cof_grant_divisor to_articles_stocks[operation.article] -= operation.article_nb else: - if operationgroup.on_acc.is_cash: - data["errors"]["account"] = "LIQ" + if on_acc.is_cash: + data["errors"].append( + { + "code": "invalid_liq", + "message": ( + "Impossible de compter autre chose que des achats sur LIQ" + ), + } + ) if operation.type != Operation.EDIT: to_checkout_balance += operation.amount operationgroup.amount += operation.amount @@ -1090,41 +1098,42 @@ def kpsul_perform_operations(request): if operation.type == Operation.EDIT: required_perms.add("kfet.edit_balance_account") need_comment = True - if operationgroup.on_acc.is_cof: + if on_acc.is_cof: to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor - (perms, stop) = operationgroup.on_acc.perms_to_perform_operation( - amount=operationgroup.amount - ) + (perms, stop) = on_acc.perms_to_perform_operation(amount=operationgroup.amount) required_perms |= perms + if stop: + data["errors"].append( + { + "code": "negative", + "message": f"Le compte {on_acc.trigramme} a un solde insuffisant.", + } + ) + if need_comment: operationgroup.comment = operationgroup.comment.strip() if not operationgroup.comment: - data["errors"]["need_comment"] = True + data["need_comment"] = True - if data["errors"]: + if data["errors"] or "need_comment" in data: return JsonResponse(data, status=400) - if stop 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: - data["errors"]["negative"] = [operationgroup.on_acc.trigramme] + if not request.user.has_perms(required_perms): + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) # If 1 perm is required, filling who perform the operations if required_perms: operationgroup.valid_by = request.user.profile.account_kfet # Filling cof status for statistics - operationgroup.is_cof = operationgroup.on_acc.is_cof + operationgroup.is_cof = on_acc.is_cof # Starting transaction to ensure data consistency with transaction.atomic(): # If not cash account, # saving account's balance and adding to Negative if not in - on_acc = operationgroup.on_acc if not on_acc.is_cash: ( Account.objects.filter(pk=on_acc.pk).update( @@ -1148,13 +1157,10 @@ def kpsul_perform_operations(request): # Saving operation group operationgroup.save() - data["operationgroup"] = operationgroup.pk - # Filling operationgroup id for each operations and saving for operation in operations: operation.group = operationgroup operation.save() - data["operations"].append(operation.pk) # Updating articles stock for article in to_articles_stocks: @@ -1177,7 +1183,7 @@ def kpsul_perform_operations(request): "valid_by__trigramme": ( operationgroup.valid_by and operationgroup.valid_by.trigramme or None ), - "on_acc__trigramme": operationgroup.on_acc.trigramme, + "on_acc__trigramme": on_acc.trigramme, "entries": [], } ] @@ -1218,7 +1224,7 @@ def kpsul_perform_operations(request): @kfet_password_auth def 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: @@ -1227,29 +1233,41 @@ def cancel_operations(request): map(int, filter(None, request.POST.getlist("operations[]", []))) ) except ValueError: + data["errors"].append( + {"code": "invalid_request", "message": "Requête invalide !"} + ) 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 + data["errors"].append( + { + "code": "cancel_missing", + "message": "Opérations inexistantes : {}".format( + ", ".join(map(str, opes_notexisting)) + ), + } + ) return JsonResponse(data, status=400) opes_already_canceled = [] # Déjà annulée opes = [] # 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 - 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(int) + # ------ sur les montants des groupes d'opé + to_groups_amounts = defaultdict(int) + # ------ sur les balances de caisses + to_checkouts_balances = defaultdict(int) + # ------ sur les stocks d'articles + to_articles_stocks = defaultdict(int) + for ope in opes_all: if ope.canceled_at: # Opération déjà annulée, va pour un warning en Response @@ -1320,16 +1338,22 @@ def cancel_operations(request): amount=to_accounts_balances[account] ) required_perms |= perms - stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if stop_all or not request.user.has_perms(required_perms): - missing_perms = get_missing_perms(required_perms, request.user) - if missing_perms: - data["errors"]["missing_perms"] = missing_perms - if stop_all: - data["errors"]["negative"] = negative_accounts + if negative_accounts: + data["errors"].append( + { + "code": "negative", + "message": "Solde insuffisant pour les comptes suivants : {}".format( + ", ".join(negative_accounts) + ), + } + ) + return JsonResponse(data, status=400) + + if not request.user.has_perms(required_perms): + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None @@ -1657,12 +1681,36 @@ def transfers_create(request): @teamkfet_required @kfet_password_auth def perform_transfers(request): - data = {"errors": {}, "transfers": [], "transfergroup": 0} + data = {"errors": []} # Checking transfer_formset transfer_formset = TransferFormSet(request.POST) - if not transfer_formset.is_valid(): - return JsonResponse({"errors": list(transfer_formset.errors)}, status=400) + try: + if not transfer_formset.is_valid(): + for form_errors in transfer_formset.errors: + for (field, errors) in form_errors.items(): + if field == "amount": + for error in errors: + data["errors"].append({"code": "amount", "message": error}) + else: + # C'est compliqué de trouver le compte qui pose problème... + acc_error = True + + if acc_error: + data["errors"].append( + { + "code": "invalid_acc", + "message": "L'un des comptes est invalide ou manquant", + } + ) + + return JsonResponse(data, status=400) + + except ValidationError: + data["errors"].append( + {"code": "invalid_request", "message": "Requête invalide"} + ) + return JsonResponse(data, status=400) transfers = transfer_formset.save(commit=False) @@ -1670,14 +1718,12 @@ def perform_transfers(request): required_perms = set( ["kfet.add_transfer"] ) # Required perms to perform all transfers - to_accounts_balances = defaultdict(lambda: 0) # For balances of accounts + to_accounts_balances = defaultdict(int) # For balances of accounts for transfer in transfers: to_accounts_balances[transfer.from_acc] -= transfer.amount to_accounts_balances[transfer.to_acc] += transfer.amount - stop_all = False - negative_accounts = [] # Checking if ok on all accounts frozen = set() @@ -1689,20 +1735,34 @@ def perform_transfers(request): amount=to_accounts_balances[account] ) required_perms |= perms - stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if len(frozen): - data["errors"]["frozen"] = list(frozen) + if frozen: + data["errors"].append( + { + "code": "frozen", + "message": "Les comptes suivants sont gelés : {}".format( + ", ".join(frozen) + ), + } + ) + + if negative_accounts: + data["errors"].append( + { + "code": "negative", + "message": "Solde insuffisant pour les comptes suivants : {}".format( + ", ".join(negative_accounts) + ), + } + ) + + if data["errors"]: return JsonResponse(data, status=400) - 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 + if not request.user.has_perms(required_perms): + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) # Creating transfer group @@ -1724,22 +1784,20 @@ def perform_transfers(request): # Saving transfer group transfergroup.save() - data["transfergroup"] = transfergroup.pk # Saving all transfers with group for transfer in transfers: transfer.group = transfergroup transfer.save() - data["transfers"].append(transfer.pk) - return JsonResponse(data) + return JsonResponse({}) @teamkfet_required @kfet_password_auth def cancel_transfers(request): # Pour la réponse - data = {"canceled": [], "warnings": {}, "errors": {}} + data = {"canceled": [], "warnings": {}, "errors": []} # Checking if BAD REQUEST (transfers_pk not int or not existing) try: @@ -1748,7 +1806,11 @@ def cancel_transfers(request): map(int, filter(None, request.POST.getlist("transfers[]", []))) ) except ValueError: + data["errors"].append( + {"code": "invalid_request", "message": "Requête invalide !"} + ) 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) @@ -1757,17 +1819,23 @@ def cancel_transfers(request): transfer for transfer in transfers_post if transfer not in transfers_pk ] if transfers_notexisting: - data["errors"]["transfers_notexisting"] = transfers_notexisting + data["errors"].append( + { + "code": "cancel_missing", + "message": "Transferts inexistants : {}".format( + ", ".join(map(str, transfers_notexisting)) + ), + } + ) return JsonResponse(data, status=400) - transfers_already_canceled = [] # Déjà annulée - transfers = [] # Pas déjà annulée + transfers_already_canceled = [] # Déjà annulés + transfers = [] # Pas déjà annulés 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 + + # Modifs à faire sur les balances des comptes + to_accounts_balances = defaultdict(int) for transfer in transfers_all: if transfer.canceled_at: # Transfert déjà annulé, va pour un warning en Response @@ -1795,16 +1863,22 @@ def cancel_transfers(request): amount=to_accounts_balances[account] ) required_perms |= perms - stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if stop_all or not request.user.has_perms(required_perms): - missing_perms = get_missing_perms(required_perms, request.user) - if missing_perms: - data["errors"]["missing_perms"] = missing_perms - if stop_all: - data["errors"]["negative"] = negative_accounts + if negative_accounts: + data["errors"].append( + { + "code": "negative", + "message": "Solde insuffisant pour les comptes suivants : {}".format( + ", ".join(negative_accounts) + ), + } + ) + return JsonResponse(data, status=400) + + if not request.user.has_perms(required_perms): + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None