From 2c2f82a0f72241b4a909594362a470b89814c0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 9 Aug 2016 11:02:26 +0200 Subject: [PATCH] Ajout des annulations sur K-Psul --- kfet/migrations/0025_auto_20160809_0750.py | 18 ++++ kfet/migrations/0026_auto_20160809_0810.py | 19 ++++ kfet/models.py | 31 ++++-- kfet/templates/kfet/kpsul.html | 36 ++++++- kfet/urls.py | 2 + kfet/views.py | 117 ++++++++++++++++++++- 6 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 kfet/migrations/0025_auto_20160809_0750.py create mode 100644 kfet/migrations/0026_auto_20160809_0810.py diff --git a/kfet/migrations/0025_auto_20160809_0750.py b/kfet/migrations/0025_auto_20160809_0750.py new file mode 100644 index 00000000..8ba90e2d --- /dev/null +++ b/kfet/migrations/0025_auto_20160809_0750.py @@ -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}, + ), + ] diff --git a/kfet/migrations/0026_auto_20160809_0810.py b/kfet/migrations/0026_auto_20160809_0810.py new file mode 100644 index 00000000..7e96c937 --- /dev/null +++ b/kfet/migrations/0026_auto_20160809_0810.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', '0025_auto_20160809_0750'), + ] + + operations = [ + migrations.AlterField( + model_name='settings', + name='name', + field=models.CharField(db_index=True, max_length=45, unique=True), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 152a048c..f1128b18 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -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() diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index a33c55a3..af9fb073 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -39,6 +39,13 @@ +
+ Opé annul 1:
+ Opé annul 2: +
+ + + diff --git a/kfet/urls.py b/kfet/urls.py index 43701ef6..c0368f8f 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -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'), ] diff --git a/kfet/views.py b/kfet/views.py index 651db541..4c5ad69f 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -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)