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

View file

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