Gestion des commandes K-Psul donnant un négatif

* 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...
This commit is contained in:
Aurélien Delobelle 2016-08-08 07:44:05 +02:00
parent 897986fec8
commit 510e16eecf
7 changed files with 183 additions and 22 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,15 @@ from django.core.exceptions import PermissionDenied, ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from django.utils.six.moves import reduce from django.utils.six.moves import reduce
import datetime from django.utils import timezone
from datetime import date, timedelta
import re import re
def choices_length(choices): def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
def default_promo(): def default_promo():
now = datetime.date.today() now = date.today()
return now.month <= 8 and now.year-1 or now.year return now.month <= 8 and now.year-1 or now.year
class Account(models.Model): class Account(models.Model):
@ -28,7 +29,7 @@ class Account(models.Model):
default = 0) default = 0)
is_frozen = models.BooleanField(default = False) is_frozen = models.BooleanField(default = False)
# Optional # 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( promo = models.IntegerField(
choices = PROMO_CHOICES, choices = PROMO_CHOICES,
blank = True, null = True, default = default_promo()) blank = True, null = True, default = default_promo())
@ -95,12 +96,32 @@ class Account(models.Model):
def perms_to_perform_operation(self, amount): def perms_to_perform_operation(self, amount):
perms = [] perms = []
stop_ope = False
# Checking is frozen account
if self.is_frozen: if self.is_frozen:
perms.append('kfet.override_frozen_protection') perms.append('kfet.override_frozen_protection')
new_balance = self.balance + amount 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') perms.append('kfet.perform_negative_operations')
return perms return perms, stop_ope
# Surcharge Méthode save() avec gestions de User et CofProfile # Surcharge Méthode save() avec gestions de User et CofProfile
# Args: # Args:
@ -109,7 +130,7 @@ class Account(models.Model):
# - Enregistre User, CofProfile à partir de "data" # - Enregistre User, CofProfile à partir de "data"
# - Enregistre Account # - Enregistre Account
def save(self, data = {}, *args, **kwargs): def save(self, data = {}, *args, **kwargs):
if self.pk: if self.pk and data:
# Account update # Account update
# Updating User with data # Updating User with data
@ -122,7 +143,7 @@ class Account(models.Model):
cof = self.cofprofile cof = self.cofprofile
cof.departement = data.get("departement", cof.departement) cof.departement = data.get("departement", cof.departement)
cof.save() cof.save()
else: elif data:
# New account # New account
# Checking if user has already an account # Checking if user has already an account
@ -151,7 +172,8 @@ class Account(models.Model):
if "departement" in data: if "departement" in data:
cof.departement = data['departement'] cof.departement = data['departement']
cof.save() cof.save()
self.cofprofile = cof if data:
self.cofprofile = cof
super(Account, self).save(*args, **kwargs) super(Account, self).save(*args, **kwargs)
# Surcharge de delete # Surcharge de delete
@ -168,13 +190,16 @@ class AccountNegative(models.Model):
account = models.OneToOneField( account = models.OneToOneField(
Account, on_delete = models.PROTECT, Account, on_delete = models.PROTECT,
related_name = "negative") related_name = "negative")
start = models.DateTimeField(default = datetime.datetime.now) start = models.DateTimeField(
blank = True, null = True, default = None)
balance_offset = models.DecimalField( balance_offset = models.DecimalField(
max_digits = 6, decimal_places = 2, max_digits = 6, decimal_places = 2,
default = 0) blank = True, null = True, default = None)
authorized_overdraft = models.DecimalField( authz_overdraft_amount = models.DecimalField(
max_digits = 6, decimal_places = 2, 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) comment = models.CharField(max_length = 255, blank = True)
class Checkout(models.Model): class Checkout(models.Model):
@ -440,6 +465,8 @@ class Settings(models.Model):
value_account = models.ForeignKey( value_account = models.ForeignKey(
Account, on_delete = models.PROTECT, Account, on_delete = models.PROTECT,
blank = True, null = True, default = None) blank = True, null = True, default = None)
value_duration = models.DurationField(
blank = True, null = True, default = None)
@staticmethod @staticmethod
def setting_inst(name): def setting_inst(name):
@ -465,3 +492,17 @@ class Settings(models.Model):
return Settings.setting_inst("ADDCOST_FOR").value_account return Settings.setting_inst("ADDCOST_FOR").value_account
except Settings.DoesNotExist: except Settings.DoesNotExist:
return None; 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

View file

@ -10,8 +10,9 @@ from django.contrib.auth.models import User, Permission
from django.http import HttpResponse, JsonResponse, Http404 from django.http import HttpResponse, JsonResponse, Http404
from django.forms import modelformset_factory from django.forms import modelformset_factory
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.utils import timezone
from gestioncof.models import CofProfile, Clipper 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 kfet.forms import *
from collections import defaultdict from collections import defaultdict
@ -455,15 +456,15 @@ def kpsul_perform_operations(request):
# Using select_for_update where it is critical # Using select_for_update where it is critical
try: try:
with transaction.atomic(): 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) on_acc = Account.objects.select_for_update().get(pk=on_acc.pk)
# Adding required permissions to perform operation group # 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) amount = operationgroup.amount)
required_perms += opegroup_perms required_perms += opegroup_perms
# Checking authenticated user has all 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 raise PermissionDenied
# If 1 perm is required, saving who perform the operations # If 1 perm is required, saving who perform the operations
@ -473,9 +474,22 @@ def kpsul_perform_operations(request):
# Filling cof status for statistics # Filling cof status for statistics
operationgroup.is_cof = on_acc.is_cof 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 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() on_acc.save()
# Saving addcost_for with new balance if there is one # Saving addcost_for with new balance if there is one
if is_addcost: if is_addcost:
addcost_for.balance += addcost_total addcost_for.balance += addcost_total
@ -494,16 +508,16 @@ def kpsul_perform_operations(request):
operation.article.save() operation.article.save()
data['operations'].append(operation.pk) data['operations'].append(operation.pk)
except PermissionDenied: except PermissionDenied:
# Sending BAD_REQUEST with missing perms # Sending BAD_REQUEST with missing perms or url to manage negative
missing_perms = \ missing_perms = \
[ Permission.objects.get(codename=codename).name for codename in ( [ Permission.objects.get(codename=codename).name for codename in (
(perm.split('.'))[1] for perm in (perm.split('.'))[1] for perm in
required_perms if not request.user.has_perm(perm) 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) return JsonResponse(data, status=403)
except IntegrityError:
data['errors'].append('DB error')
return JsonResponse(data, status=500)
return JsonResponse(data) return JsonResponse(data)