From 510e16eecf073859c066dc3fc2d48b7d3d3e89d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 8 Aug 2016 07:44:05 +0200 Subject: [PATCH] =?UTF-8?q?Gestion=20des=20commandes=20K-Psul=20donnant=20?= =?UTF-8?q?un=20n=C3=A9gatif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Settings - New: OVERDRAFT_AMOUNT Découvert autorisé par défaut - New: OVERDRAFT_DURATION Durée maximum d'un découvert par défaut * K-Psul : Gestion des commandes aboutissant à un négatif - Si une commande aboutit à un nouveau solde négatif, demande la permission 'kfet.perform_negative_operations' - Si le total de la commande est négatif, vérifie que ni la contrainte de temps de découvert, ni celle de montant maximum n'est outrepassée. Si ce n'est pas le cas, la commande ne peut être enregistrée jusqu'à définir des "règles de négatif" pour le compte concerné. La durée maximum d'un découvert est celle dans AccountNegative si elle y est définie pour le compte concerné, sinon celle par défaut (Settings.OVERDRAFT_DURATION). Il en est de même pour le découvert maximum autorisé. Attention: le découvert doit être exprimé sous forme de valeur positive aussi bien dans AccountNegative que pour Settings.OVERDRAFT_AMOUNT. - Si les permissions nécessaires sont présentes, qu'il n'y a pas de blocage et que le compte n'a pas encore d'entrée dans AccountNegative, création d'une entrée avec start=now() - Si la balance d'un compte est positive après une commande, supprime l'entrée dans AccountNegative associée au compte si le "décalage de zéro" (donné par balance_offset) est nul. Sinon cela veut dire que le compte n'est pas réellement en positif. * Modèles - Fix: Account.save() fonctionne dans le cas où data est vide - Modif: AccountNegative - Valeurs par défaut, NULL... --- kfet/migrations/0020_auto_20160808_0450.py | 20 ++++++ kfet/migrations/0021_auto_20160808_0506.py | 19 ++++++ kfet/migrations/0022_auto_20160808_0512.py | 24 +++++++ kfet/migrations/0023_auto_20160808_0535.py | 24 +++++++ .../0024_settings_value_duration.py | 19 ++++++ kfet/models.py | 65 +++++++++++++++---- kfet/views.py | 34 +++++++--- 7 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 kfet/migrations/0020_auto_20160808_0450.py create mode 100644 kfet/migrations/0021_auto_20160808_0506.py create mode 100644 kfet/migrations/0022_auto_20160808_0512.py create mode 100644 kfet/migrations/0023_auto_20160808_0535.py create mode 100644 kfet/migrations/0024_settings_value_duration.py diff --git a/kfet/migrations/0020_auto_20160808_0450.py b/kfet/migrations/0020_auto_20160808_0450.py new file mode 100644 index 00000000..2ecc18ee --- /dev/null +++ b/kfet/migrations/0020_auto_20160808_0450.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0019_auto_20160808_0343'), + ] + + operations = [ + migrations.AlterField( + model_name='accountnegative', + name='start', + field=models.DateTimeField(default=datetime.datetime.now, blank=True, null=True), + ), + ] diff --git a/kfet/migrations/0021_auto_20160808_0506.py b/kfet/migrations/0021_auto_20160808_0506.py new file mode 100644 index 00000000..61a7ef65 --- /dev/null +++ b/kfet/migrations/0021_auto_20160808_0506.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0020_auto_20160808_0450'), + ] + + operations = [ + migrations.AlterField( + model_name='accountnegative', + name='start', + field=models.DateTimeField(default=None, blank=True, null=True), + ), + ] diff --git a/kfet/migrations/0022_auto_20160808_0512.py b/kfet/migrations/0022_auto_20160808_0512.py new file mode 100644 index 00000000..ba5de03e --- /dev/null +++ b/kfet/migrations/0022_auto_20160808_0512.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0021_auto_20160808_0506'), + ] + + operations = [ + migrations.AlterField( + model_name='accountnegative', + name='authorized_overdraft', + field=models.DecimalField(blank=True, decimal_places=2, null=True, default=None, max_digits=6), + ), + migrations.AlterField( + model_name='accountnegative', + name='balance_offset', + field=models.DecimalField(blank=True, decimal_places=2, null=True, default=None, max_digits=6), + ), + ] diff --git a/kfet/migrations/0023_auto_20160808_0535.py b/kfet/migrations/0023_auto_20160808_0535.py new file mode 100644 index 00000000..7e4d7051 --- /dev/null +++ b/kfet/migrations/0023_auto_20160808_0535.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0022_auto_20160808_0512'), + ] + + operations = [ + migrations.RenameField( + model_name='accountnegative', + old_name='authorized_overdraft', + new_name='authz_overdraft_amount', + ), + migrations.AddField( + model_name='accountnegative', + name='authz_overdraft_until', + field=models.DateTimeField(null=True, default=None, blank=True), + ), + ] diff --git a/kfet/migrations/0024_settings_value_duration.py b/kfet/migrations/0024_settings_value_duration.py new file mode 100644 index 00000000..1f90b1c9 --- /dev/null +++ b/kfet/migrations/0024_settings_value_duration.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0023_auto_20160808_0535'), + ] + + operations = [ + migrations.AddField( + model_name='settings', + name='value_duration', + field=models.DurationField(null=True, default=None, blank=True), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 5f740d27..10564aa9 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -4,14 +4,15 @@ from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import RegexValidator from gestioncof.models import CofProfile from django.utils.six.moves import reduce -import datetime +from django.utils import timezone +from datetime import date, timedelta import re def choices_length(choices): return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) def default_promo(): - now = datetime.date.today() + now = date.today() return now.month <= 8 and now.year-1 or now.year class Account(models.Model): @@ -28,7 +29,7 @@ class Account(models.Model): default = 0) is_frozen = models.BooleanField(default = False) # Optional - PROMO_CHOICES = [(r,r) for r in range(1980, datetime.date.today().year+1)] + PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)] promo = models.IntegerField( choices = PROMO_CHOICES, blank = True, null = True, default = default_promo()) @@ -95,12 +96,32 @@ class Account(models.Model): def perms_to_perform_operation(self, amount): perms = [] + stop_ope = False + # Checking is frozen account if self.is_frozen: perms.append('kfet.override_frozen_protection') new_balance = self.balance + amount - if new_balance < 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 = - Settings.OVERDRAFT_AMOUNT() + # 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 + Settings.OVERDRAFT_DURATION() + else: + overdraft_until = timezone.now() + Settings.OVERDRAFT_DURATION() + # Checking it doesn't break 1 rule + if new_balance < overdraft_amount or timezone.now() > overdraft_until: + stop_ope = True perms.append('kfet.perform_negative_operations') - return perms + return perms, stop_ope # Surcharge Méthode save() avec gestions de User et CofProfile # Args: @@ -109,7 +130,7 @@ class Account(models.Model): # - Enregistre User, CofProfile à partir de "data" # - Enregistre Account def save(self, data = {}, *args, **kwargs): - if self.pk: + if self.pk and data: # Account update # Updating User with data @@ -122,7 +143,7 @@ class Account(models.Model): cof = self.cofprofile cof.departement = data.get("departement", cof.departement) cof.save() - else: + elif data: # New account # Checking if user has already an account @@ -151,7 +172,8 @@ class Account(models.Model): if "departement" in data: cof.departement = data['departement'] cof.save() - self.cofprofile = cof + if data: + self.cofprofile = cof super(Account, self).save(*args, **kwargs) # Surcharge de delete @@ -168,13 +190,16 @@ class AccountNegative(models.Model): account = models.OneToOneField( Account, on_delete = models.PROTECT, related_name = "negative") - start = models.DateTimeField(default = datetime.datetime.now) + start = models.DateTimeField( + blank = True, null = True, default = None) balance_offset = models.DecimalField( max_digits = 6, decimal_places = 2, - default = 0) - authorized_overdraft = models.DecimalField( + blank = True, null = True, default = None) + authz_overdraft_amount = models.DecimalField( max_digits = 6, decimal_places = 2, - default = 0) + blank = True, null = True, default = None) + authz_overdraft_until = models.DateTimeField( + blank = True, null = True, default = None) comment = models.CharField(max_length = 255, blank = True) class Checkout(models.Model): @@ -440,6 +465,8 @@ class Settings(models.Model): value_account = models.ForeignKey( Account, on_delete = models.PROTECT, blank = True, null = True, default = None) + value_duration = models.DurationField( + blank = True, null = True, default = None) @staticmethod def setting_inst(name): @@ -465,3 +492,17 @@ class Settings(models.Model): return Settings.setting_inst("ADDCOST_FOR").value_account except Settings.DoesNotExist: return None; + + @staticmethod + def OVERDRAFT_DURATION(): + try: + return Settings.setting_inst("OVERDRAFT_DURATION").value_duration + except Settings.DoesNotExist: + return timedelta() + + @staticmethod + def OVERDRAFT_AMOUNT(): + try: + return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal + except Settings.DoesNotExist: + return 0 diff --git a/kfet/views.py b/kfet/views.py index a5d330fb..ca1fbb68 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -10,8 +10,9 @@ from django.contrib.auth.models import User, Permission from django.http import HttpResponse, JsonResponse, Http404 from django.forms import modelformset_factory from django.db import IntegrityError, transaction +from django.utils import timezone from gestioncof.models import CofProfile, Clipper -from kfet.models import Account, Checkout, Article, Settings +from kfet.models import Account, Checkout, Article, Settings, AccountNegative from kfet.forms import * from collections import defaultdict @@ -455,15 +456,15 @@ def kpsul_perform_operations(request): # Using select_for_update where it is critical try: with transaction.atomic(): - on_acc = operationgroup.on_acc + on_acc = operationgroup.on_acc on_acc = Account.objects.select_for_update().get(pk=on_acc.pk) # Adding required permissions to perform operation group - opegroup_perms = on_acc.perms_to_perform_operation( + (opegroup_perms, stop_ope) = on_acc.perms_to_perform_operation( amount = operationgroup.amount) required_perms += opegroup_perms # Checking authenticated user has all perms - if not request.user.has_perms(required_perms): + if stop_ope or not request.user.has_perms(required_perms): raise PermissionDenied # If 1 perm is required, saving who perform the operations @@ -473,9 +474,22 @@ def kpsul_perform_operations(request): # Filling cof status for statistics operationgroup.is_cof = on_acc.is_cof - # Saving account's balance + # Saving account's balance and adding to Negative if not in on_acc.balance += operationgroup.amount + if on_acc.balance < 0: + if hasattr(on_acc, 'negative'): + if not on_acc.negative.start: + on_acc.negative.start = timezone.now() + on_acc.negative.save() + else: + negative = AccountNegative( + account = on_acc, start = timezone.now()) + negative.save() + elif (hasattr(on_acc, 'negative') + and not on_acc.negative.balance_offset): + on_acc.negative.delete() on_acc.save() + # Saving addcost_for with new balance if there is one if is_addcost: addcost_for.balance += addcost_total @@ -494,16 +508,16 @@ def kpsul_perform_operations(request): operation.article.save() data['operations'].append(operation.pk) except PermissionDenied: - # Sending BAD_REQUEST with missing perms + # Sending BAD_REQUEST with missing perms or url to manage negative missing_perms = \ [ Permission.objects.get(codename=codename).name for codename in ( (perm.split('.'))[1] for perm in required_perms if not request.user.has_perm(perm) )] - data['errors'].append({'missing_perms': missing_perms }) + if missing_perms: + data['errors'].append({'missing_perms': missing_perms }) + if stop_ope: + data['errors'].append({'negative': 'url to manage negative'}) return JsonResponse(data, status=403) - except IntegrityError: - data['errors'].append('DB error') - return JsonResponse(data, status=500) return JsonResponse(data)