Ajout des annulations sur K-Psul

This commit is contained in:
Aurélien Delobelle 2016-08-09 11:02:26 +02:00
parent 070752bd01
commit 2c2f82a0f7
6 changed files with 210 additions and 13 deletions

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('kfet', '0024_settings_value_duration'),
]
operations = [
migrations.AlterModelOptions(
name='globalpermissions',
options={'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes')), 'managed': False},
),
]

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', '0025_auto_20160809_0750'),
]
operations = [
migrations.AlterField(
model_name='settings',
name='name',
field=models.CharField(db_index=True, max_length=45, unique=True),
),
]

View file

@ -99,8 +99,13 @@ class Account(models.Model):
data['is_free'] = True data['is_free'] = True
return data return data
def perms_to_perform_operation(self, amount): def perms_to_perform_operation(self, amount, overdraft_duration_max=None, \
perms = [] overdraft_amount_max=None):
if overdraft_duration_max is None:
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
if overdraft_amount_max is None:
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
perms = set()
stop_ope = False stop_ope = False
# Checking is cash account # Checking is cash account
if self.is_cash: if self.is_cash:
@ -108,7 +113,7 @@ class Account(models.Model):
return [], False return [], False
# Checking is frozen account # Checking is frozen account
if self.is_frozen: if self.is_frozen:
perms.append('kfet.override_frozen_protection') perms.add('kfet.override_frozen_protection')
new_balance = self.balance + amount new_balance = self.balance + amount
if new_balance < 0 and amount < 0: if new_balance < 0 and amount < 0:
# Retrieving overdraft amount limit # Retrieving overdraft amount limit
@ -116,20 +121,20 @@ class Account(models.Model):
and self.negative.authz_overdraft_amount is not None): and self.negative.authz_overdraft_amount is not None):
overdraft_amount = - self.negative.authz_overdraft_amount overdraft_amount = - self.negative.authz_overdraft_amount
else: else:
overdraft_amount = - Settings.OVERDRAFT_AMOUNT() overdraft_amount = - overdraft_amount_max
# Retrieving overdraft datetime limit # Retrieving overdraft datetime limit
if (hasattr(self, 'negative') if (hasattr(self, 'negative')
and self.negative.authz_overdraft_until is not None): and self.negative.authz_overdraft_until is not None):
overdraft_until = self.negative.authz_overdraft_until overdraft_until = self.negative.authz_overdraft_until
elif hasattr(self, 'negative'): elif hasattr(self, 'negative'):
overdraft_until = \ overdraft_until = \
self.negative.start + Settings.OVERDRAFT_DURATION() self.negative.start + overdraft_duration_max
else: else:
overdraft_until = timezone.now() + Settings.OVERDRAFT_DURATION() overdraft_until = timezone.now() + overdraft_duration_max
# Checking it doesn't break 1 rule # Checking it doesn't break 1 rule
if new_balance < overdraft_amount or timezone.now() > overdraft_until: if new_balance < overdraft_amount_max or timezone.now() > overdraft_until:
stop_ope = True stop_ope = True
perms.append('kfet.perform_negative_operations') perms.add('kfet.perform_negative_operations')
return perms, stop_ope return perms, stop_ope
# Surcharge Méthode save() avec gestions de User et CofProfile # Surcharge Méthode save() avec gestions de User et CofProfile
@ -462,12 +467,14 @@ class GlobalPermissions(models.Model):
('perform_negative_operations', ('perform_negative_operations',
'Enregistrer des commandes en négatif'), 'Enregistrer des commandes en négatif'),
('override_frozen_protection', "Forcer le gel d'un compte"), ('override_frozen_protection', "Forcer le gel d'un compte"),
('cancel_old_operations', 'Annuler des commandes non récentes'),
) )
class Settings(models.Model): class Settings(models.Model):
name = models.CharField( name = models.CharField(
max_length = 45, max_length = 45,
unique = True) unique = True,
db_index = True)
value_decimal = models.DecimalField( value_decimal = models.DecimalField(
max_digits = 6, decimal_places = 2, max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None) blank = True, null = True, default = None)
@ -515,3 +522,9 @@ class Settings(models.Model):
return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
except Settings.DoesNotExist: except Settings.DoesNotExist:
return 0 return 0
def CANCEL_DURATION():
try:
return Settings.setting_inst("CANCEL_DURATION").value_duration
except Settings.DoesNotExist:
return timedelta()

View file

@ -39,6 +39,13 @@
<button type="button" id="perform_operations">Valider</button> <button type="button" id="perform_operations">Valider</button>
<form id="cancel_form">
Opé annul 1:<input type="text" name="operation"><br>
Opé annul 2:<input type="text" name="operation">
</form>
<button type="button" id="cancel_operations">Annuler</button>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// ----- // -----
@ -188,7 +195,7 @@ $(document).ready(function() {
var operations = $('#operation_formset'); var operations = $('#operation_formset');
function performOperations() { function performOperations() {
data = operationGroup.serialize() + '&' + operations.serialize(); var data = operationGroup.serialize() + '&' + operations.serialize();
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url : "{% url 'kfet.kpsul.perform_operations' %}", url : "{% url 'kfet.kpsul.perform_operations' %}",
@ -208,6 +215,33 @@ $(document).ready(function() {
performButton.on('click', function() { performButton.on('click', function() {
performOperations(); performOperations();
}); });
// Cancel operations
var cancelButton = $('#cancel_operations');
var cancelForm = $('#cancel_form');
function cancelOperations() {
var data = cancelForm.serialize();
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.cancel_operations' %}",
method : "POST",
data : data,
})
.done(function(data) {
console.log(data);
})
.always(function($xhr) {
var data = $xhr.responseJSON;
console.log(data);
});
}
// Event listeners
cancelButton.on('click', function() {
cancelOperations();
});
}); });
</script> </script>

View file

@ -90,4 +90,6 @@ urlpatterns = [
name = 'kfet.kpsul.checkout_data'), name = 'kfet.kpsul.checkout_data'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations, url('^k-psul/perform_operations$', views.kpsul_perform_operations,
name = 'kfet.kpsul.perform_operations'), name = 'kfet.kpsul.perform_operations'),
url('^k-psul/cancel_operations$', views.kpsul_cancel_operations,
name = 'kfet.kpsul.cancel_operations'),
] ]

View file

@ -10,6 +10,7 @@ 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.db.models import F
from django.utils import timezone 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, AccountNegative from kfet.models import Account, Checkout, Article, Settings, AccountNegative
@ -385,6 +386,15 @@ def kpsul_checkout_data(request):
'valid_from': checkout.valid_from, 'valid_to': checkout.valid_to } 'valid_from': checkout.valid_from, 'valid_to': checkout.valid_to }
return JsonResponse(data) return JsonResponse(data)
def get_missing_perms(required_perms, user):
missing_perms_codenames = [ (perm.split('.'))[1]
for perm in required_perms if not user.has_perm(perm)]
missing_perms = list(
Permission.objects
.filter(codename__in=missing_perms_codenames)
.values_list('name', flat=True))
return missing_perms
@permission_required('kfet.is_team') @permission_required('kfet.is_team')
def kpsul_perform_operations(request): def kpsul_perform_operations(request):
# Initializing response data # Initializing response data
@ -423,7 +433,7 @@ def kpsul_perform_operations(request):
addcost_for = Settings.ADDCOST_FOR() addcost_for = Settings.ADDCOST_FOR()
# Initializing vars # Initializing vars
required_perms = [] required_perms = set()
cof_grant_divisor = 1 + cof_grant / 100 cof_grant_divisor = 1 + cof_grant / 100
is_addcost = (addcost_for and addcost_amount is_addcost = (addcost_for and addcost_amount
and addcost_for != operationgroup.on_acc) and addcost_for != operationgroup.on_acc)
@ -467,7 +477,7 @@ def kpsul_perform_operations(request):
operationgroup.amount += operation.amount operationgroup.amount += operation.amount
# 4 # 4
if operation.type == Operation.DEPOSIT: if operation.type == Operation.DEPOSIT:
required_perms.append('kfet.perform_deposit') required_perms.add('kfet.perform_deposit')
# Starting transaction to ensure data consistency # Starting transaction to ensure data consistency
# Using select_for_update where it is critical # Using select_for_update where it is critical
@ -478,7 +488,7 @@ def kpsul_perform_operations(request):
# Adding required permissions to perform operation group # Adding required permissions to perform operation group
(opegroup_perms, stop_ope) = 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 stop_ope or not request.user.has_perms(required_perms): if stop_ope or not request.user.has_perms(required_perms):
@ -544,3 +554,104 @@ def kpsul_perform_operations(request):
return JsonResponse(data, status=403) return JsonResponse(data, status=403)
return JsonResponse(data) return JsonResponse(data)
@permission_required('kfet.is_team')
def kpsul_cancel_operations(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (opes_pk not int or not existing)
try:
# Set pour virer les doublons
opes_post = set(map(int, filter(None, request.POST.getlist('operation', []))))
except ValueError:
return JsonResponse(data, status=400)
opes_all = Operation.objects.select_related('group', 'group__on_acc', 'group__on_acc__negative').filter(pk__in=opes_post)
opes_pk = [ ope.pk for ope in opes_all ]
opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ]
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
return JsonResponse(data, status=400)
opes_already_canceled = [] # Déjà annulée
opes = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = Settings.CANCEL_DURATION()
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles
for ope in opes_all:
if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response
opes_already_canceled.append(ope.pk)
else:
opes.append(ope.pk)
# Si opé il y a plus de CANCEL_DURATION, permission requise
if ope.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
if not ope.group.on_acc.is_cash:
to_accounts_balances[ope.group.on_acc] -= ope.amount
if ope.addcost_for and ope.addcost_amount:
to_accounts_balances[ope.addcost_for] -= ope.addcost_amount
# Pour les groupes d'opés
to_groups_amounts[ope.group] -= ope.amount
# Pour les balances de caisses
if ope.type == Operation.PURCHASE:
if ope.group.on_acc.is_cash:
to_checkouts_balances[ope.group.on_acc] -= - ope.amount
else:
to_checkouts_balances[ope.group.on_acc] -= ope.amount
# Pour les stocks d'articles
if ope.article and ope.article_nb:
to_articles_stocks[ope.article] += ope.article_nb
if not opes:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)
# Checking permissions or stop
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account],
overdraft_duration_max = overdraft_duration_max,
overdraft_amount_max = overdraft_amount_max)
required_perms |= perms
stop_all = stop_all or stop
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = True
return JsonResponse(data, status=403)
with transaction.atomic():
canceled_by = required_perms and request.user.profile.account_kfet or None
(Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=timezone.now()))
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
for checkout in to_checkouts_balances:
Checkout.objects.filter(pk=checkout.pk).update(
balance = F('balance') + to_checkouts_balances[checkout])
for group in to_groups_amounts:
OperationGroup.objects.filter(pk=group.pk).update(
amount = F('amount') + to_groups_amounts[group])
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
stock = F('stock') + to_articles_stocks[article])
data['canceled'] = opes
if opes_already_canceled:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)