Merge branch 'Aufinal/remove_negative' into 'master'

Fonctionnement du négatif + erreurs de K-Psul

Closes #279

See merge request klub-dev-ens/gestioCOF!494
This commit is contained in:
Tom Hubrecht 2021-06-17 19:22:14 +00:00
commit a5c822e7f7
13 changed files with 450 additions and 491 deletions

View file

@ -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:

View file

@ -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),
),
]

View file

@ -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):

View file

@ -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;

View file

@ -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 += '<ul>';
if (data['errors']['operation_group'].indexOf('on_acc') != -1)
content += '<li>Pas de compte sélectionné</li>';
if (data['errors']['operation_group'].indexOf('checkout') != -1)
content += '<li>Pas de caisse sélectionnée</li>';
content += '</ul>';
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 += '<ul>';
for (var i = 0; i < data['errors']['missing_perms'].length; i++)
content += '<li>' + data['errors']['missing_perms'][i] + '</li>';
content += '</ul>';
}
if ('negative' in data['errors']) {
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 += '<a class="btn btn-primary" href="' + url_base + data['errors']['negative'][i] + '/edit" target="_blank" style="width:100%">Autorisation de négatif requise pour ' + data['errors']['negative'][i] + '</a>';
}
}
if ('addcost' in data['errors']) {
content += '<ul>';
if (data['errors']['addcost'].indexOf('__all__') != -1)
content += '<li>Compte invalide</li>';
if (data['errors']['addcost'].indexOf('amount') != -1)
content += '<li>Montant invalide</li>';
content += '</ul>';
}
if ('account' in data['errors']) {
content += 'Général';
content += '<ul>';
content += '<li>Opération invalide sur le compte ' + data['errors']['account'] + '</li>';
content += '</ul>';
}
if ('frozen' in data['errors']) {
content += 'Général';
content += '<ul>';
content += '<li>Les comptes suivants sont gelés : ' + data['errors']['frozen'].join(", ") + '</li>';
content += '</ul>';
var content = is_error ? "Général :" : "Permissions manquantes :";
content += "<ul>";
for (const message of data) {
content += '<li>' + message + '</li>';
}
content += "</ul>";
return content;
}
function requestAuth(data, callback, focus_next = null) {
var content = getErrorsHtml(data);
content += '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>',
$.confirm({
title: 'Authentification requise',
content: content,
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 += '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>';
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',

View file

@ -10,26 +10,12 @@
{{ negatives|length }}
<span class="sub">compte{{ negatives|length|pluralize }} en négatif</span>
</div>
<div class="text">
<b>Total:</b> {{ negatives_sum|floatformat:2 }}€
</div>
<div class="text">
<b>Plafond par défaut</b>
<ul class="list-unstyled">
<li>Montant: {{ kfet_config.overdraft_amount }}€</li>
<li>Pendant: {{ kfet_config.overdraft_duration }}</li>
</ul>
<div class="heading">
{{ negatives_sum|floatformat:2 }}€
<span class="sub">de négatif total</span>
</div>
</aside>
{% if perms.kfet.change_settings %}
<div class="buttons">
<div class="full">
<button type="button" class="btn btn-primary" href="{% url 'kfet.settings' %}">Modifier les valeurs par défaut</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block main %}
@ -43,8 +29,6 @@
<td>Nom</td>
<td class="text-right">Balance</td>
<td data-sorter="shortDate">Début</td>
<td>Découvert autorisé</td>
<td data-sorter="shortDate">Jusqu'au</td>
</tr>
</thead>
<tbody>
@ -60,10 +44,6 @@
<td title="{{ neg.start }}">
{{ neg.start|date:'d/m/Y H:i'}}
</td>
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
<td title="{{ neg.authz_overdraft_until }}">
{{ neg.authz_overdraft_until|date:'d/m/Y H:i' }}
</td>
</tr>
{% endfor %}
</tbody>

View file

@ -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" %}
</form>
<script type="text/javascript">
$(document).ready(function () {
$('#id_authz_overdraft_until').datetimepicker({
format: 'YYYY-MM-DD HH:mm',
stepping: 5,
locale: 'fr',
});
});
</script>
{% endblock %}

View file

@ -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;
}
});

View file

@ -54,12 +54,6 @@
{% if account.negative.start %}
<li>Depuis le <b>{{ account.negative.start|date:"d/m/Y à H:i" }}</b></li>
{% endif %}
<li>
Plafond :
<b>{{ account.negative.authz_overdraft_amount|default:kfet_config.overdraft_amount }} €</b>
jusqu'au
<b>{{ account.negative.authz_overdraft_until|default:account.negative.until_default|date:"d/m/Y à H:i" }}</b>
</li>
</ul>
</div>
{% endif %}

View file

@ -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;
}
});

View file

@ -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):

View file

@ -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": [
{

View file

@ -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