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
return data
def perms_to_perform_operation(self, amount):
perms = []
def perms_to_perform_operation(self, amount, overdraft_duration_max=None, \
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
# Checking is cash account
if self.is_cash:
@ -108,7 +113,7 @@ class Account(models.Model):
return [], False
# Checking is frozen account
if self.is_frozen:
perms.append('kfet.override_frozen_protection')
perms.add('kfet.override_frozen_protection')
new_balance = self.balance + amount
if new_balance < 0 and amount < 0:
# Retrieving overdraft amount limit
@ -116,20 +121,20 @@ class Account(models.Model):
and self.negative.authz_overdraft_amount is not None):
overdraft_amount = - self.negative.authz_overdraft_amount
else:
overdraft_amount = - Settings.OVERDRAFT_AMOUNT()
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 + Settings.OVERDRAFT_DURATION()
self.negative.start + overdraft_duration_max
else:
overdraft_until = timezone.now() + Settings.OVERDRAFT_DURATION()
overdraft_until = timezone.now() + overdraft_duration_max
# 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
perms.append('kfet.perform_negative_operations')
perms.add('kfet.perform_negative_operations')
return perms, stop_ope
# Surcharge Méthode save() avec gestions de User et CofProfile
@ -462,12 +467,14 @@ class GlobalPermissions(models.Model):
('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'),
)
class Settings(models.Model):
name = models.CharField(
max_length = 45,
unique = True)
unique = True,
db_index = True)
value_decimal = models.DecimalField(
max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None)
@ -515,3 +522,9 @@ class Settings(models.Model):
return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
except Settings.DoesNotExist:
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>
<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">
$(document).ready(function() {
// -----
@ -188,7 +195,7 @@ $(document).ready(function() {
var operations = $('#operation_formset');
function performOperations() {
data = operationGroup.serialize() + '&' + operations.serialize();
var data = operationGroup.serialize() + '&' + operations.serialize();
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.perform_operations' %}",
@ -208,6 +215,33 @@ $(document).ready(function() {
performButton.on('click', function() {
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>

View file

@ -90,4 +90,6 @@ urlpatterns = [
name = 'kfet.kpsul.checkout_data'),
url('^k-psul/perform_operations$', views.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.forms import modelformset_factory
from django.db import IntegrityError, transaction
from django.db.models import F
from django.utils import timezone
from gestioncof.models import CofProfile, Clipper
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 }
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')
def kpsul_perform_operations(request):
# Initializing response data
@ -423,7 +433,7 @@ def kpsul_perform_operations(request):
addcost_for = Settings.ADDCOST_FOR()
# Initializing vars
required_perms = []
required_perms = set()
cof_grant_divisor = 1 + cof_grant / 100
is_addcost = (addcost_for and addcost_amount
and addcost_for != operationgroup.on_acc)
@ -467,7 +477,7 @@ def kpsul_perform_operations(request):
operationgroup.amount += operation.amount
# 4
if operation.type == Operation.DEPOSIT:
required_perms.append('kfet.perform_deposit')
required_perms.add('kfet.perform_deposit')
# Starting transaction to ensure data consistency
# Using select_for_update where it is critical
@ -478,7 +488,7 @@ def kpsul_perform_operations(request):
# Adding required permissions to perform operation group
(opegroup_perms, stop_ope) = on_acc.perms_to_perform_operation(
amount = operationgroup.amount)
required_perms += opegroup_perms
required_perms |= opegroup_perms
# Checking authenticated user has all 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)
@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)