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 gestioncof.models import CofProfile
from kfet.models import ( from kfet.models import (
Account, Account,
AccountNegative,
Article, Article,
ArticleCategory, ArticleCategory,
Checkout, Checkout,
@ -158,17 +157,6 @@ class UserInfoForm(UserForm):
fields = ["first_name", "last_name"] 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 # Checkout forms
# ----- # -----
@ -549,7 +537,7 @@ class TransferForm(forms.ModelForm):
def clean_amount(self): def clean_amount(self):
amount = self.cleaned_data["amount"] amount = self.cleaned_data["amount"]
if amount <= 0: if amount <= 0:
raise forms.ValidationError("Montant invalide") raise forms.ValidationError("Le montant d'un transfert doit être positif")
return amount return amount
class Meta: 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 return data
def perms_to_perform_operation(self, amount): def perms_to_perform_operation(self, amount):
overdraft_duration_max = kfet_config.overdraft_duration
overdraft_amount_max = kfet_config.overdraft_amount
perms = set() perms = set()
stop_ope = False
# Checking is cash account # Checking is cash account
if self.is_cash: if self.is_cash:
# Yes, so no perms and no stop # Yes, so no perms and no stop
return set(), False return set(), False
if self.need_comment: if self.need_comment:
perms.add("kfet.perform_commented_operations") perms.add("kfet.perform_commented_operations")
new_balance = self.balance + amount new_balance = self.balance + amount
if new_balance < -kfet_config.overdraft_amount:
return set(), True
if new_balance < 0 and amount < 0: 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") perms.add("kfet.perform_negative_operations")
return perms, stop_ope
return perms, False
# Surcharge Méthode save() avec gestions de User et CofProfile # Surcharge Méthode save() avec gestions de User et CofProfile
# Args: # Args:
@ -267,17 +249,26 @@ class Account(models.Model):
def update_negative(self): def update_negative(self):
if self.balance < 0: 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.start = timezone.now()
self.negative.end = None
self.negative.save() self.negative.save()
elif not hasattr(self, "negative"): elif not hasattr(self, "negative"):
self.negative = AccountNegative.objects.create( self.negative = AccountNegative.objects.create(
account=self, start=timezone.now() account=self, start=timezone.now()
) )
elif hasattr(self, "negative"): elif hasattr(self, "negative"):
# self.balance >= 0 if self.negative.end is None:
# TODO: méchanisme pour éviter de contourner le délai de négatif ? self.negative.end = timezone.now()
self.negative.delete() 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): class UserHasAccount(Exception):
def __init__(self, trigramme): def __init__(self, trigramme):
@ -302,26 +293,11 @@ class AccountNegative(models.Model):
Account, on_delete=models.CASCADE, related_name="negative" Account, on_delete=models.CASCADE, related_name="negative"
) )
start = models.DateTimeField(blank=True, null=True, default=None) start = models.DateTimeField(blank=True, null=True, default=None)
authz_overdraft_amount = models.DecimalField( end = models.DateTimeField(blank=True, null=True, default=None)
"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)
class Meta: class Meta:
permissions = (("view_negs", "Voir la liste des négatifs"),) 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): class CheckoutQuerySet(models.QuerySet):
def is_valid(self): def is_valid(self):

View file

@ -257,11 +257,11 @@ function KHistory(options = {}) {
switch ($xhr.status) { switch ($xhr.status) {
case 403: case 403:
requestAuth(data, function (password) { requestAuth(data, function (password) {
this.cancel(opes, password); that._cancel(type, opes, password);
}); });
break; break;
case 400: case 400:
displayErrors(getErrorsHtml(data)); displayErrors(data);
break; break;
} }
window.lock = 0; window.lock = 0;

View file

@ -106,116 +106,80 @@ function amountToUKF(amount, is_cof = false, account = false) {
return rounding(amount * coef_cof * 10); return rounding(amount * coef_cof * 10);
} }
function getErrorsHtml(data) { function getErrorsHtml(data, is_error = true) {
var content = ''; if (is_error) {
if (!data) data = data.map(error => error.message)
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>';
} }
if ('missing_perms' in data['errors']) {
content += 'Permissions manquantes'; var content = is_error ? "Général :" : "Permissions manquantes :";
content += '<ul>'; content += "<ul>";
for (var i = 0; i < data['errors']['missing_perms'].length; i++) for (const message of data) {
content += '<li>' + data['errors']['missing_perms'][i] + '</li>'; content += '<li>' + message + '</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>';
} }
content += "</ul>";
return content; return content;
} }
function requestAuth(data, callback, focus_next = null) { function requestAuth(data, callback, focus_next = null) {
var content = getErrorsHtml(data); 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>', 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 s = String.fromCharCode(e.which); $.confirm({
if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey) || //caps on, shift off title: 'Authentification requise',
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on content: content,
capslock = 1; backgroundDismiss: true,
} else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey) || //caps off, shift off animation: 'top',
(s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on closeAnimation: 'bottom',
capslock = 0; keyboardEnabled: true,
} confirm: function () {
if (capslock == 1) var password = this.$content.find('input').val();
$('.capslock .glyphicon').show(); callback(password);
else if (capslock == 0) },
$('.capslock .glyphicon').hide(); onOpen: function () {
}); var that = this;
// Capslock key is not detected by keypress var capslock = -1; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown
this.$content.find('input').on('keydown', function (e) { this.$content.find('input').on('keypress', function (e) {
if (e.which == 20) { if (e.keyCode == 13)
capslock = 1 - capslock; that.$confirmButton.click();
}
if (capslock == 1)
$('.capslock .glyphicon').show();
else if (capslock == 0)
$('.capslock .glyphicon').hide();
});
},
onClose: function () {
if (focus_next)
this._lastFocused = focus_next;
}
}); 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({ $.alert({
title: 'Erreurs', title: 'Erreurs',
content: html, content: content,
backgroundDismiss: true, backgroundDismiss: true,
animation: 'top', animation: 'top',
closeAnimation: 'bottom', closeAnimation: 'bottom',

View file

@ -10,26 +10,12 @@
{{ negatives|length }} {{ negatives|length }}
<span class="sub">compte{{ negatives|length|pluralize }} en négatif</span> <span class="sub">compte{{ negatives|length|pluralize }} en négatif</span>
</div> </div>
<div class="text"> <div class="heading">
<b>Total:</b> {{ negatives_sum|floatformat:2 }}€ {{ negatives_sum|floatformat:2 }}€
</div> <span class="sub">de négatif total</span>
<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> </div>
</aside> </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 %} {% endblock %}
{% block main %} {% block main %}
@ -43,8 +29,6 @@
<td>Nom</td> <td>Nom</td>
<td class="text-right">Balance</td> <td class="text-right">Balance</td>
<td data-sorter="shortDate">Début</td> <td data-sorter="shortDate">Début</td>
<td>Découvert autorisé</td>
<td data-sorter="shortDate">Jusqu'au</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -60,10 +44,6 @@
<td title="{{ neg.start }}"> <td title="{{ neg.start }}">
{{ neg.start|date:'d/m/Y H:i'}} {{ neg.start|date:'d/m/Y H:i'}}
</td> </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> </tr>
{% endfor %} {% endfor %}
</tbody> </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=frozen_form %}
{% include 'kfet/form_snippet.html' with form=group_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=pwd_form %}
{% include 'kfet/form_snippet.html' with form=negative_form %}
{% if perms.kfet.is_team %}
{% include 'kfet/form_authentication_snippet.html' %} {% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %} {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form> </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 %} {% endblock %}

View file

@ -376,10 +376,10 @@ $(document).ready(function() {
requestAuth(data, performOperations, articleSelect); requestAuth(data, performOperations, articleSelect);
break; break;
case 400: case 400:
if ('need_comment' in data['errors']) { if ('need_comment' in data) {
askComment(performOperations); askComment(performOperations);
} else { } else {
displayErrors(getErrorsHtml(data)); displayErrors(data);
} }
break; break;
} }
@ -1074,7 +1074,7 @@ $(document).ready(function() {
}, triInput); }, triInput);
break; break;
case 400: case 400:
askAddcost(getErrorsHtml(data)); askAddcost(getErrorsHtml(data["errors"], is_error=true));
break; break;
} }
}); });

View file

@ -54,12 +54,6 @@
{% if account.negative.start %} {% if account.negative.start %}
<li>Depuis le <b>{{ account.negative.start|date:"d/m/Y à H:i" }}</b></li> <li>Depuis le <b>{{ account.negative.start|date:"d/m/Y à H:i" }}</b></li>
{% endif %} {% 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> </ul>
</div> </div>
{% endif %} {% endif %}

View file

@ -72,7 +72,7 @@ $(document).ready(function () {
var $next = $form.next('.transfer_form').find('.from_acc input'); var $next = $form.next('.transfer_form').find('.from_acc input');
} }
var $input_id = $input.next('input'); var $input_id = $input.next('input');
if (isValidTrigramme(trigramme)) { if (trigramme.is_valid_trigramme()) {
getAccountData(trigramme, function(data) { getAccountData(trigramme, function(data) {
$input_id.val(data.id); $input_id.val(data.id);
$data.text(data.name); $data.text(data.name);
@ -122,7 +122,7 @@ $(document).ready(function () {
requestAuth(data, performTransfers); requestAuth(data, performTransfers);
break; break;
case 400: case 400:
displayErrors(getErrorsHtml(data)); displayErrors(data);
break; 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.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from kfet.models import Account, Checkout from kfet.models import Account, AccountNegative, Checkout
from .utils import create_user from .utils import create_user
@ -28,6 +30,56 @@ class AccountTests(TestCase):
with self.assertRaises(Account.DoesNotExist): with self.assertRaises(Account.DoesNotExist):
Account.objects.get_by_password("bernard") 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): class CheckoutTests(TestCase):
def setUp(self): def setUp(self):
@ -39,7 +91,7 @@ class CheckoutTests(TestCase):
self.c = Checkout( self.c = Checkout(
created_by=self.u_acc, created_by=self.u_acc,
valid_from=self.now, valid_from=self.now,
valid_to=self.now + datetime.timedelta(days=1), valid_to=self.now + timedelta(days=1),
) )
def test_initial_statement(self): def test_initial_statement(self):

View file

@ -15,7 +15,6 @@ from ..auth.utils import hash_password
from ..config import kfet_config from ..config import kfet_config
from ..models import ( from ..models import (
Account, Account,
AccountNegative,
Article, Article,
ArticleCategory, ArticleCategory,
Checkout, Checkout,
@ -1856,7 +1855,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_group_on_acc_expects_comment(self):
user_add_perms(self.users["team"], ["kfet.perform_commented_operations"]) user_add_perms(self.users["team"], ["kfet.perform_commented_operations"])
@ -1899,7 +1901,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_invalid_group_on_acc_needs_comment_requires_perm(self):
self.account.trigramme = "#13" self.account.trigramme = "#13"
@ -1922,8 +1924,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertEqual(
json_data["errors"]["missing_perms"], json_data["missing_perms"],
["[kfet] Enregistrer des commandes avec commentaires"], ["Enregistrer des commandes avec commentaires"],
) )
def test_error_on_acc_frozen(self): def test_error_on_acc_frozen(self):
@ -1945,7 +1947,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_invalid_group_checkout(self):
self.checkout.valid_from -= timedelta(days=300) self.checkout.valid_from -= timedelta(days=300)
@ -1957,7 +1959,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_invalid_group_expects_one_operation(self):
data = dict(self.base_post_data) data = dict(self.base_post_data)
@ -1965,7 +1970,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_purchase_with_user_is_nof_cof(self):
self.account.cofprofile.is_cof = False self.account.cofprofile.is_cof = False
@ -2023,12 +2031,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
# Check response content # Check response content
self.assertDictEqual( self.assertDictEqual(
json_data, json_data,
{ {"errors": []},
"operationgroup": operation_group.pk,
"operations": [operation.pk],
"warnings": {},
"errors": {},
},
) )
# Check object updates # Check object updates
@ -2179,9 +2182,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [e["code"] for e in json_data["errors"]],
[{"__all__": ["Un achat nécessite un article et une quantité"]}], ["invalid_formset"],
) )
def test_invalid_purchase_expects_article_nb(self): def test_invalid_purchase_expects_article_nb(self):
@ -2199,9 +2202,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [e["code"] for e in json_data["errors"]],
[{"__all__": ["Un achat nécessite un article et une quantité"]}], ["invalid_formset"],
) )
def test_invalid_purchase_expects_article_nb_greater_than_1(self): 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) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [e["code"] for e in json_data["errors"]],
[ ["invalid_formset"],
{
"__all__": ["Un achat nécessite un article et une quantité"],
"article_nb": [
"Assurez-vous que cette valeur est supérieure ou " "égale à 1."
],
}
],
) )
def test_invalid_operation_not_purchase_with_cash(self): def test_invalid_operation_not_purchase_with_cash(self):
@ -2247,7 +2243,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_deposit(self):
user_add_perms(self.users["team"], ["kfet.perform_deposit"]) user_add_perms(self.users["team"], ["kfet.perform_deposit"])
@ -2300,12 +2299,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertDictEqual( self.assertDictEqual(
json_data, json_data,
{ {"errors": []},
"operationgroup": operation_group.pk,
"operations": [operation.pk],
"warnings": {},
"errors": {},
},
) )
self.account.refresh_from_db() self.account.refresh_from_db()
@ -2364,8 +2358,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] [e["code"] for e in json_data["errors"]],
["invalid_formset"],
) )
def test_invalid_deposit_too_many_params(self): def test_invalid_deposit_too_many_params(self):
@ -2383,8 +2378,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] [e["code"] for e in json_data["errors"]],
["invalid_formset"],
) )
def test_invalid_deposit_expects_positive_amount(self): def test_invalid_deposit_expects_positive_amount(self):
@ -2402,8 +2398,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [{"__all__": ["Charge non positive"]}] [e["code"] for e in json_data["errors"]],
["invalid_formset"],
) )
def test_invalid_deposit_requires_perm(self): def test_invalid_deposit_requires_perm(self):
@ -2421,9 +2418,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertEqual(json_data["missing_perms"], ["Effectuer une charge"])
json_data["errors"]["missing_perms"], ["[kfet] Effectuer une charge"]
)
def test_withdraw(self): def test_withdraw(self):
data = dict( data = dict(
@ -2475,12 +2470,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertDictEqual( self.assertDictEqual(
json_data, json_data,
{ {"errors": []},
"operationgroup": operation_group.pk,
"operations": [operation.pk],
"warnings": {},
"errors": {},
},
) )
self.account.refresh_from_db() self.account.refresh_from_db()
@ -2539,8 +2529,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] [e["code"] for e in json_data["errors"]],
["invalid_formset"],
) )
def test_invalid_withdraw_too_many_params(self): def test_invalid_withdraw_too_many_params(self):
@ -2558,8 +2549,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] [e["code"] for e in json_data["errors"]],
["invalid_formset"],
) )
def test_invalid_withdraw_expects_negative_amount(self): def test_invalid_withdraw_expects_negative_amount(self):
@ -2577,8 +2569,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertCountEqual(
json_data["errors"]["operations"], [{"__all__": ["Retrait non négatif"]}] [e["code"] for e in json_data["errors"]],
["invalid_formset"],
) )
def test_edit(self): def test_edit(self):
@ -2634,12 +2627,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertDictEqual( self.assertDictEqual(
json_data, json_data,
{ {"errors": []},
"operationgroup": operation_group.pk,
"operations": [operation.pk],
"warnings": {},
"errors": {},
},
) )
self.account.refresh_from_db() self.account.refresh_from_db()
@ -2700,8 +2688,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertEqual(
json_data["errors"]["missing_perms"], json_data["missing_perms"],
["[kfet] Modifier la balance d'un compte"], ["Modifier la balance d'un compte"],
) )
def test_invalid_edit_expects_comment(self): def test_invalid_edit_expects_comment(self):
@ -2721,7 +2709,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def _setup_addcost(self):
self.register_user("addcost", create_user("addcost", "ADD")) self.register_user("addcost", create_user("addcost", "ADD"))
@ -3008,62 +2996,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertEqual(
json_data["errors"], json_data["missing_perms"],
{"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, ["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): def test_invalid_negative_exceeds_amount_allowed_from_config(self):
user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
kfet_config.set(overdraft_amount=Decimal("-1.00")) kfet_config.set(overdraft_amount=Decimal("-1.00"))
@ -3083,38 +3019,13 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
) )
resp = self.client.post(self.url, data) 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")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual(json_data["errors"], {"negative": ["000"]}) self.assertCountEqual(
[e["code"] for e in json_data["errors"]],
def test_invalid_negative_exceeds_amount_allowed_from_account(self): ["negative"],
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"),
) )
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): def test_multi_0(self):
article2 = Article.objects.create( article2 = Article.objects.create(
name="Article 2", name="Article 2",
@ -3198,12 +3109,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
# Check response content # Check response content
self.assertDictEqual( self.assertDictEqual(
json_data, json_data,
{ {"errors": []},
"operationgroup": operation_group.pk,
"operations": [operation_list[0].pk, operation_list[1].pk],
"warnings": {},
"errors": {},
},
) )
# Check object updates # Check object updates
@ -3342,7 +3248,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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): def test_invalid_operation_not_exist(self):
data = {"operations[]": ["1000"]} data = {"operations[]": ["1000"]}
@ -3350,7 +3259,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
json_data = json.loads(resp.content.decode("utf-8")) 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") @mock.patch("django.utils.timezone.now")
def test_purchase(self, now_mock): def test_purchase(self, now_mock):
@ -3414,7 +3326,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
"canceled_by__trigramme": None, "canceled_by__trigramme": None,
} }
], ],
"errors": {}, "errors": [],
"warnings": {}, "warnings": {},
"opegroups_to_update": [ "opegroups_to_update": [
{ {
@ -3602,7 +3514,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
"canceled_by__trigramme": None, "canceled_by__trigramme": None,
} }
], ],
"errors": {}, "errors": [],
"warnings": {}, "warnings": {},
"opegroups_to_update": [ "opegroups_to_update": [
{ {
@ -3689,7 +3601,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
"canceled_by__trigramme": None, "canceled_by__trigramme": None,
} }
], ],
"errors": {}, "errors": [],
"warnings": {}, "warnings": {},
"opegroups_to_update": [ "opegroups_to_update": [
{ {
@ -3776,7 +3688,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
"canceled_by__trigramme": None, "canceled_by__trigramme": None,
} }
], ],
"errors": {}, "errors": [],
"warnings": {}, "warnings": {},
"opegroups_to_update": [ "opegroups_to_update": [
{ {
@ -3839,8 +3751,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertEqual(
json_data["errors"], json_data["missing_perms"],
{"missing_perms": ["[kfet] Annuler des commandes non récentes"]}, ["Annuler des commandes non récentes"],
) )
def test_already_canceled(self): def test_already_canceled(self):
@ -3964,9 +3876,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
data = {"operations[]": [str(operation.pk)]} data = {"operations[]": [str(operation.pk)]}
resp = self.client.post(self.url, data) 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")) 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): def test_invalid_negative_requires_perms(self):
kfet_config.set(overdraft_amount=Decimal("40.00")) kfet_config.set(overdraft_amount=Decimal("40.00"))
@ -3985,8 +3900,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
json_data = json.loads(resp.content.decode("utf-8")) json_data = json.loads(resp.content.decode("utf-8"))
self.assertEqual( self.assertEqual(
json_data["errors"], json_data["missing_perms"],
{"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, ["Enregistrer des commandes en négatif"],
) )
def test_partial_0(self): def test_partial_0(self):
@ -4036,7 +3951,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
"canceled_by__trigramme": None, "canceled_by__trigramme": None,
}, },
], ],
"errors": {}, "errors": [],
"warnings": {"already_canceled": [operation3.pk]}, "warnings": {"already_canceled": [operation3.pk]},
"opegroups_to_update": [ "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.core.exceptions import SuspiciousOperation, ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum 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 ( from django.http import (
Http404, Http404,
HttpResponseBadRequest, HttpResponseBadRequest,
@ -39,7 +39,6 @@ from kfet.decorators import teamkfet_required
from kfet.forms import ( from kfet.forms import (
AccountForm, AccountForm,
AccountFrozenForm, AccountFrozenForm,
AccountNegativeForm,
AccountNoTriForm, AccountNoTriForm,
AccountPwdForm, AccountPwdForm,
AccountStatForm, AccountStatForm,
@ -355,11 +354,6 @@ def account_update(request, trigramme):
frozen_form = AccountFrozenForm(request.POST, instance=account) frozen_form = AccountFrozenForm(request.POST, instance=account)
pwd_form = AccountPwdForm() pwd_form = AccountPwdForm()
if hasattr(account, "negative"):
negative_form = AccountNegativeForm(instance=account.negative)
else:
negative_form = None
if request.method == "POST": if request.method == "POST":
self_update = request.user == account.user self_update = request.user == account.user
account_form = AccountForm(request.POST, instance=account) account_form = AccountForm(request.POST, instance=account)
@ -381,14 +375,6 @@ def account_update(request, trigramme):
elif group_form.has_changed(): elif group_form.has_changed():
warnings.append("statut d'équipe") 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 # Il ne faut pas valider `pwd_form` si elle est inchangée
if pwd_form.has_changed(): if pwd_form.has_changed():
if self_update or request.user.has_perm("kfet.change_account_password"): 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, "account_form": account_form,
"frozen_form": frozen_form, "frozen_form": frozen_form,
"group_form": group_form, "group_form": group_form,
"negative_form": negative_form,
"pwd_form": pwd_form, "pwd_form": pwd_form,
}, },
) )
@ -482,9 +467,11 @@ class AccountDelete(PermissionRequiredMixin, DeleteView):
class AccountNegativeList(ListView): class AccountNegativeList(ListView):
queryset = AccountNegative.objects.select_related( queryset = (
"account", "account__cofprofile__user" AccountNegative.objects.select_related("account", "account__cofprofile__user")
).exclude(account__trigramme="#13") .filter(account__balance__lt=0)
.exclude(account__trigramme="#13")
)
template_name = "kfet/account_negative.html" template_name = "kfet/account_negative.html"
context_object_name = "negatives" context_object_name = "negatives"
@ -977,15 +964,18 @@ def kpsul_checkout_data(request):
@kfet_password_auth @kfet_password_auth
def kpsul_update_addcost(request): def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST) addcost_form = AddcostForm(request.POST)
data = {"errors": []}
if not addcost_form.is_valid(): 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) return JsonResponse(data, status=400)
required_perms = ["kfet.manage_addcosts"] required_perms = ["kfet.manage_addcosts"]
if not request.user.has_perms(required_perms): if not request.user.has_perms(required_perms):
data = { data["missing_perms"] = get_missing_perms(required_perms, request.user)
"errors": {"missing_perms": get_missing_perms(required_perms, request.user)}
}
return JsonResponse(data, status=403) return JsonResponse(data, status=403)
trigramme = addcost_form.cleaned_data["trigramme"] 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_missing_perms(required_perms: List[str], user: User) -> List[str]:
def get_perm_description(app_label: str, codename: str) -> str: def get_perm_name(app_label: str, codename: str) -> str:
name = Permission.objects.values_list("name", flat=True).get( return Permission.objects.values_list("name", flat=True).get(
codename=codename, content_type__app_label=app_label codename=codename, content_type__app_label=app_label
) )
return "[{}] {}".format(app_label, name)
missing_perms = [ missing_perms = [
get_perm_description(*perm.split(".")) get_perm_name(*perm.split("."))
for perm in required_perms for perm in required_perms
if not user.has_perm(perm) 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 @kfet_password_auth
def kpsul_perform_operations(request): def kpsul_perform_operations(request):
# Initializing response data # Initializing response data
data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}} data = {"errors": []}
# Checking operationgroup # Checking operationgroup
operationgroup_form = KPsulOperationGroupForm(request.POST) operationgroup_form = KPsulOperationGroupForm(request.POST)
if not operationgroup_form.is_valid(): 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 # Checking operation_formset
operation_formset = KPsulOperationFormSet(request.POST) operation_formset = KPsulOperationFormSet(request.POST)
if not operation_formset.is_valid(): 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 # Returning BAD REQUEST if errors
if data["errors"]: if data["errors"]:
@ -1038,6 +1041,7 @@ def kpsul_perform_operations(request):
# Pre-saving (no commit) # Pre-saving (no commit)
operationgroup = operationgroup_form.save(commit=False) operationgroup = operationgroup_form.save(commit=False)
operations = operation_formset.save(commit=False) operations = operation_formset.save(commit=False)
on_acc = operationgroup.on_acc
# Retrieving COF grant # Retrieving COF grant
cof_grant = kfet_config.subvention_cof 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_addcost_for_balance = 0 # For balance of addcost_for
to_checkout_balance = 0 # For balance of selected checkout to_checkout_balance = 0 # For balance of selected checkout
to_articles_stocks = defaultdict(lambda: 0) # For stocks articles to_articles_stocks = defaultdict(lambda: 0) # For stocks articles
is_addcost = all( is_addcost = all((addcost_for, addcost_amount, addcost_for != on_acc))
(addcost_for, addcost_amount, addcost_for != operationgroup.on_acc) need_comment = on_acc.need_comment
)
need_comment = operationgroup.on_acc.need_comment
if operationgroup.on_acc.is_frozen: if on_acc.is_frozen:
data["errors"]["frozen"] = [operationgroup.on_acc.trigramme] data["errors"].append(
{"code": "frozen_acc", "message": f"Le compte {on_acc.trigramme} est gelé"}
)
# Filling data of each operations # Filling data of each operations
# + operationgroup + calculating other stuffs # + operationgroup + calculating other stuffs
@ -1069,19 +1073,23 @@ def kpsul_perform_operations(request):
operation.addcost_amount = addcost_amount * operation.article_nb operation.addcost_amount = addcost_amount * operation.article_nb
operation.amount -= operation.addcost_amount operation.amount -= operation.addcost_amount
to_addcost_for_balance += 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 to_checkout_balance += -operation.amount
if ( if on_acc.is_cof and operation.article.category.has_reduction:
operationgroup.on_acc.is_cof
and operation.article.category.has_reduction
):
if is_addcost and operation.article.category.has_addcost: if is_addcost and operation.article.category.has_addcost:
operation.addcost_amount /= cof_grant_divisor operation.addcost_amount /= cof_grant_divisor
operation.amount = operation.amount / cof_grant_divisor operation.amount = operation.amount / cof_grant_divisor
to_articles_stocks[operation.article] -= operation.article_nb to_articles_stocks[operation.article] -= operation.article_nb
else: else:
if operationgroup.on_acc.is_cash: if on_acc.is_cash:
data["errors"]["account"] = "LIQ" data["errors"].append(
{
"code": "invalid_liq",
"message": (
"Impossible de compter autre chose que des achats sur LIQ"
),
}
)
if operation.type != Operation.EDIT: if operation.type != Operation.EDIT:
to_checkout_balance += operation.amount to_checkout_balance += operation.amount
operationgroup.amount += operation.amount operationgroup.amount += operation.amount
@ -1090,41 +1098,42 @@ def kpsul_perform_operations(request):
if operation.type == Operation.EDIT: if operation.type == Operation.EDIT:
required_perms.add("kfet.edit_balance_account") required_perms.add("kfet.edit_balance_account")
need_comment = True 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 to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
(perms, stop) = operationgroup.on_acc.perms_to_perform_operation( (perms, stop) = on_acc.perms_to_perform_operation(amount=operationgroup.amount)
amount=operationgroup.amount
)
required_perms |= perms required_perms |= perms
if stop:
data["errors"].append(
{
"code": "negative",
"message": f"Le compte {on_acc.trigramme} a un solde insuffisant.",
}
)
if need_comment: if need_comment:
operationgroup.comment = operationgroup.comment.strip() operationgroup.comment = operationgroup.comment.strip()
if not operationgroup.comment: 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) return JsonResponse(data, status=400)
if stop or not request.user.has_perms(required_perms): if not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user) data["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]
return JsonResponse(data, status=403) return JsonResponse(data, status=403)
# If 1 perm is required, filling who perform the operations # If 1 perm is required, filling who perform the operations
if required_perms: if required_perms:
operationgroup.valid_by = request.user.profile.account_kfet operationgroup.valid_by = request.user.profile.account_kfet
# Filling cof status for statistics # 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 # Starting transaction to ensure data consistency
with transaction.atomic(): with transaction.atomic():
# If not cash account, # If not cash account,
# saving account's balance and adding to Negative if not in # saving account's balance and adding to Negative if not in
on_acc = operationgroup.on_acc
if not on_acc.is_cash: if not on_acc.is_cash:
( (
Account.objects.filter(pk=on_acc.pk).update( Account.objects.filter(pk=on_acc.pk).update(
@ -1148,13 +1157,10 @@ def kpsul_perform_operations(request):
# Saving operation group # Saving operation group
operationgroup.save() operationgroup.save()
data["operationgroup"] = operationgroup.pk
# Filling operationgroup id for each operations and saving # Filling operationgroup id for each operations and saving
for operation in operations: for operation in operations:
operation.group = operationgroup operation.group = operationgroup
operation.save() operation.save()
data["operations"].append(operation.pk)
# Updating articles stock # Updating articles stock
for article in to_articles_stocks: for article in to_articles_stocks:
@ -1177,7 +1183,7 @@ def kpsul_perform_operations(request):
"valid_by__trigramme": ( "valid_by__trigramme": (
operationgroup.valid_by and operationgroup.valid_by.trigramme or None operationgroup.valid_by and operationgroup.valid_by.trigramme or None
), ),
"on_acc__trigramme": operationgroup.on_acc.trigramme, "on_acc__trigramme": on_acc.trigramme,
"entries": [], "entries": [],
} }
] ]
@ -1218,7 +1224,7 @@ def kpsul_perform_operations(request):
@kfet_password_auth @kfet_password_auth
def cancel_operations(request): def cancel_operations(request):
# Pour la réponse # Pour la réponse
data = {"canceled": [], "warnings": {}, "errors": {}} data = {"canceled": [], "warnings": {}, "errors": []}
# Checking if BAD REQUEST (opes_pk not int or not existing) # Checking if BAD REQUEST (opes_pk not int or not existing)
try: try:
@ -1227,29 +1233,41 @@ def cancel_operations(request):
map(int, filter(None, request.POST.getlist("operations[]", []))) map(int, filter(None, request.POST.getlist("operations[]", [])))
) )
except ValueError: except ValueError:
data["errors"].append(
{"code": "invalid_request", "message": "Requête invalide !"}
)
return JsonResponse(data, status=400) return JsonResponse(data, status=400)
opes_all = Operation.objects.select_related( opes_all = Operation.objects.select_related(
"group", "group__on_acc", "group__on_acc__negative" "group", "group__on_acc", "group__on_acc__negative"
).filter(pk__in=opes_post) ).filter(pk__in=opes_post)
opes_pk = [ope.pk for ope in opes_all] opes_pk = [ope.pk for ope in opes_all]
opes_notexisting = [ope for ope in opes_post if ope not in opes_pk] opes_notexisting = [ope for ope in opes_post if ope not in opes_pk]
if opes_notexisting: 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) return JsonResponse(data, status=400)
opes_already_canceled = [] # Déjà annulée opes_already_canceled = [] # Déjà annulée
opes = [] # Pas déjà annulée opes = [] # Pas déjà annulée
required_perms = set() required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(
lambda: 0 # Modifs à faire sur les balances des comptes
) # Modifs à faire sur les balances des comptes to_accounts_balances = defaultdict(int)
to_groups_amounts = defaultdict( # ------ sur les montants des groupes d'opé
lambda: 0 to_groups_amounts = defaultdict(int)
) # ------ sur les montants des groupes d'opé # ------ sur les balances de caisses
to_checkouts_balances = defaultdict(lambda: 0) # ------ sur les balances de caisses to_checkouts_balances = defaultdict(int)
to_articles_stocks = defaultdict(lambda: 0) # ------ sur les stocks d'articles # ------ sur les stocks d'articles
to_articles_stocks = defaultdict(int)
for ope in opes_all: for ope in opes_all:
if ope.canceled_at: if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response # Opération déjà annulée, va pour un warning en Response
@ -1320,16 +1338,22 @@ def cancel_operations(request):
amount=to_accounts_balances[account] amount=to_accounts_balances[account]
) )
required_perms |= perms required_perms |= perms
stop_all = stop_all or stop
if stop: if stop:
negative_accounts.append(account.trigramme) negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms): if negative_accounts:
missing_perms = get_missing_perms(required_perms, request.user) data["errors"].append(
if missing_perms: {
data["errors"]["missing_perms"] = missing_perms "code": "negative",
if stop_all: "message": "Solde insuffisant pour les comptes suivants : {}".format(
data["errors"]["negative"] = negative_accounts ", ".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) return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None canceled_by = required_perms and request.user.profile.account_kfet or None
@ -1657,12 +1681,36 @@ def transfers_create(request):
@teamkfet_required @teamkfet_required
@kfet_password_auth @kfet_password_auth
def perform_transfers(request): def perform_transfers(request):
data = {"errors": {}, "transfers": [], "transfergroup": 0} data = {"errors": []}
# Checking transfer_formset # Checking transfer_formset
transfer_formset = TransferFormSet(request.POST) transfer_formset = TransferFormSet(request.POST)
if not transfer_formset.is_valid(): try:
return JsonResponse({"errors": list(transfer_formset.errors)}, status=400) 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) transfers = transfer_formset.save(commit=False)
@ -1670,14 +1718,12 @@ def perform_transfers(request):
required_perms = set( required_perms = set(
["kfet.add_transfer"] ["kfet.add_transfer"]
) # Required perms to perform all transfers ) # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda: 0) # For balances of accounts to_accounts_balances = defaultdict(int) # For balances of accounts
for transfer in transfers: for transfer in transfers:
to_accounts_balances[transfer.from_acc] -= transfer.amount to_accounts_balances[transfer.from_acc] -= transfer.amount
to_accounts_balances[transfer.to_acc] += transfer.amount to_accounts_balances[transfer.to_acc] += transfer.amount
stop_all = False
negative_accounts = [] negative_accounts = []
# Checking if ok on all accounts # Checking if ok on all accounts
frozen = set() frozen = set()
@ -1689,20 +1735,34 @@ def perform_transfers(request):
amount=to_accounts_balances[account] amount=to_accounts_balances[account]
) )
required_perms |= perms required_perms |= perms
stop_all = stop_all or stop
if stop: if stop:
negative_accounts.append(account.trigramme) negative_accounts.append(account.trigramme)
if len(frozen): if frozen:
data["errors"]["frozen"] = list(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) return JsonResponse(data, status=400)
if stop_all or not request.user.has_perms(required_perms): if not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user) data["missing_perms"] = get_missing_perms(required_perms, request.user)
if missing_perms:
data["errors"]["missing_perms"] = missing_perms
if stop_all:
data["errors"]["negative"] = negative_accounts
return JsonResponse(data, status=403) return JsonResponse(data, status=403)
# Creating transfer group # Creating transfer group
@ -1724,22 +1784,20 @@ def perform_transfers(request):
# Saving transfer group # Saving transfer group
transfergroup.save() transfergroup.save()
data["transfergroup"] = transfergroup.pk
# Saving all transfers with group # Saving all transfers with group
for transfer in transfers: for transfer in transfers:
transfer.group = transfergroup transfer.group = transfergroup
transfer.save() transfer.save()
data["transfers"].append(transfer.pk)
return JsonResponse(data) return JsonResponse({})
@teamkfet_required @teamkfet_required
@kfet_password_auth @kfet_password_auth
def cancel_transfers(request): def cancel_transfers(request):
# Pour la réponse # Pour la réponse
data = {"canceled": [], "warnings": {}, "errors": {}} data = {"canceled": [], "warnings": {}, "errors": []}
# Checking if BAD REQUEST (transfers_pk not int or not existing) # Checking if BAD REQUEST (transfers_pk not int or not existing)
try: try:
@ -1748,7 +1806,11 @@ def cancel_transfers(request):
map(int, filter(None, request.POST.getlist("transfers[]", []))) map(int, filter(None, request.POST.getlist("transfers[]", [])))
) )
except ValueError: except ValueError:
data["errors"].append(
{"code": "invalid_request", "message": "Requête invalide !"}
)
return JsonResponse(data, status=400) return JsonResponse(data, status=400)
transfers_all = Transfer.objects.select_related( transfers_all = Transfer.objects.select_related(
"group", "from_acc", "from_acc__negative", "to_acc", "to_acc__negative" "group", "from_acc", "from_acc__negative", "to_acc", "to_acc__negative"
).filter(pk__in=transfers_post) ).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 transfer for transfer in transfers_post if transfer not in transfers_pk
] ]
if transfers_notexisting: 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) return JsonResponse(data, status=400)
transfers_already_canceled = [] # Déjà annulée transfers_already_canceled = [] # Déjà annulés
transfers = [] # Pas déjà annulée transfers = [] # Pas déjà annulés
required_perms = set() required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(
lambda: 0 # Modifs à faire sur les balances des comptes
) # Modifs à faire sur les balances des comptes to_accounts_balances = defaultdict(int)
for transfer in transfers_all: for transfer in transfers_all:
if transfer.canceled_at: if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response # Transfert déjà annulé, va pour un warning en Response
@ -1795,16 +1863,22 @@ def cancel_transfers(request):
amount=to_accounts_balances[account] amount=to_accounts_balances[account]
) )
required_perms |= perms required_perms |= perms
stop_all = stop_all or stop
if stop: if stop:
negative_accounts.append(account.trigramme) negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms): if negative_accounts:
missing_perms = get_missing_perms(required_perms, request.user) data["errors"].append(
if missing_perms: {
data["errors"]["missing_perms"] = missing_perms "code": "negative",
if stop_all: "message": "Solde insuffisant pour les comptes suivants : {}".format(
data["errors"]["negative"] = negative_accounts ", ".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) return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None canceled_by = required_perms and request.user.profile.account_kfet or None