diff --git a/kfet/static/kfet/css/history.css b/kfet/static/kfet/css/history.css index 976f5782..64b82f96 100644 --- a/kfet/static/kfet/css/history.css +++ b/kfet/static/kfet/css/history.css @@ -40,6 +40,11 @@ width:90px; } +#history .opegroup .info { + text-align:center; + width:145px; +} + #history .opegroup .valid_by { padding-left:20px } @@ -67,6 +72,10 @@ text-align:right; } +#history .ope .glyphicon { + padding-left:15px; +} + #history .ope .infos2 { padding-left:15px; } diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 291c106d..f870b4cc 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -15,10 +15,18 @@ function KHistory(options={}) { var trigramme = opegroup['on_acc_trigramme']; var is_cof = opegroup['is_cof']; - for (var i=0; i', template_opegroup: '
', + template_transfergroup: '
', template_ope: '
', + template_transfer: '
', display_trigramme: true, } diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 31758c36..e54d0e74 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -138,3 +138,8 @@ function requestAuth(data, callback, focus_next = null) { }); } + +String.prototype.pluralize = function(count, irreg_plural = false) { + plural = irreg_plural ? irreg_plural : this + 's' ; + return (count==1 ? this : plural) ; +} diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index 091b4f2f..97a0e413 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -62,6 +62,8 @@ $(document).ready(function() { settings = { 'subvention_cof': parseFloat({{ settings.subvention_cof|unlocalize }})} + lock = 0 ; + khistory = new KHistory(); var $from_date = $('#from_date'); @@ -142,9 +144,10 @@ $(document).ready(function() { selected: function(e, ui) { $(ui.selected).each(function() { if ($(this).hasClass('opegroup')) { - var opegroup = $(this).data('opegroup'); + var type = $(this).data('type'); + var id = $(this).data('id'); $(this).siblings('.ope').filter(function() { - return $(this).data('opegroup') == opegroup + return $(this).data(type) == id }).addClass('ui-selected'); } }); @@ -156,7 +159,7 @@ $(document).ready(function() { // DEL (Suppr) var opes_to_cancel = []; khistory.$container.find('.ope.ui-selected').each(function () { - opes_to_cancel.push($(this).data('ope')); + opes_to_cancel.push($(this).data('type')+' '+$(this).data('id')); }); if (opes_to_cancel.length > 0) confirmCancel(opes_to_cancel); @@ -165,7 +168,10 @@ $(document).ready(function() { function confirmCancel(opes_to_cancel) { var nb = opes_to_cancel.length; - var content = nb+" opérations vont être annulées"; + var content = nb+' opération'.pluralize(nb) + +' va'.pluralize(nb, ' vont') + + ' être' + + ' annulée'.pluralize(nb); $.confirm({ title: 'Confirmation', content: content, @@ -179,50 +185,10 @@ $(document).ready(function() { }); } - function requestAuth(data, callback) { - var content = getErrorsHtml(data); - content += '', - $.confirm({ - title: 'Authentification requise', - content: content, - backgroundDismiss: true, - animation:'top', - closeAnimation:'bottom', - keyboardEnabled: true, - confirm: function() { - var password = this.$content.find('input').val(); - callback(password); - }, - onOpen: function() { - var that = this; - this.$content.find('input').on('keypress', function(e) { - if (e.keyCode == 13) - that.$confirmButton.click(); - }); - }, - }); - } - - function getErrorsHtml(data) { - var content = ''; - if ('missing_perms' in data['errors']) { - content += 'Permissions manquantes'; - content += ''; - } - if ('negative' in data['errors']) { - var url_base = "{% url 'kfet.account.update' LIQ}"; - url_base = base_url(0, url_base.length-8); - for (var i=0; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; - } - } - return content; - } - function cancelOperations(opes_array, password = '') { + if (lock == 1) + return false + lock = 1 ; var data = { 'operations' : opes_array } $.ajax({ dataType: "json", @@ -238,6 +204,7 @@ $(document).ready(function() { }) .done(function(data) { khistory.$container.find('.ui-selected').removeClass('ui-selected'); + lock = 0 ; }) .fail(function($xhr) { var data = $xhr.responseJSON; @@ -251,6 +218,7 @@ $(document).ready(function() { displayErrors(getErrorsHtml(data)); break; } + lock = 0 ; }); } diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html index cbdf0fe3..0ba0c85e 100644 --- a/kfet/templates/kfet/transfers.html +++ b/kfet/templates/kfet/transfers.html @@ -6,7 +6,11 @@ + + + + {% endblock %} {% block title %}Transferts{% endblock %} @@ -31,22 +35,7 @@

Liste des transferts

-
- {% for transfergroup in transfergroups %} -
- {{ transfergroup.at }} - {{ transfergroup.valid_by.trigramme }} - {{ transfergroup.comment }} -
- {% for transfer in transfergroup.transfers.all %} -
- {{ transfer.amount }} € - {{ transfer.from_acc.trigramme }} - - {{ transfer.to_acc.trigramme }} -
- {% endfor %} - {% endfor %} +
@@ -70,15 +59,80 @@ $(document).ready(function() { }); } + khistory = new KHistory({ + display_trigramme: false, + }); + + function getHistory() { + var data = {'transfersonly': true}; - function cancelTransfers(transfers_array, password = '') { - if (lock == 1) - return false - lock = 1; - var data = { 'transfers' : transfers_array } $.ajax({ dataType: "json", - url : "{% url 'kfet.transfers.cancel' %}", + url : "{% url 'kfet.history.json' %}", + method : "POST", + data : data, + }) + .done(function(data) { + for (var i=0; i 0) + confirmCancel(opes_to_cancel); + } + }); + + function confirmCancel(opes_to_cancel) { + var nb = opes_to_cancel.length; + var content = nb+' opération'.pluralize(nb) + +' va'.pluralize(nb, ' vont') + + ' être' + + ' annulée'.pluralize(nb); + $.confirm({ + title: 'Confirmation', + content: content, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + confirm: function() { + cancelOperations(opes_to_cancel); + } + }); + } + + function cancelOperations(opes_array, password = '') { + if (lock == 1) + return false + lock = 1 ; + var data = { 'operations' : opes_array } + $.ajax({ + dataType: "json", + url : "{% url 'kfet.kpsul.cancel_operations' %}", method : "POST", data : data, beforeSend: function ($xhr) { @@ -89,11 +143,7 @@ $(document).ready(function() { }) .done(function(data) { - for (var i=0; i 0) - cancelTransfers(transfers_to_cancel); - } - }); + getHistory(); }); diff --git a/kfet/urls.py b/kfet/urls.py index 271ed917..dc109606 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -178,8 +178,6 @@ urlpatterns = [ name='kfet.transfers.create'), url(r'^transfers/perform$', views.perform_transfers, name='kfet.transfers.perform'), - url(r'^transfers/cancel$', views.cancel_transfers, - name='kfet.transfers.cancel'), # ----- # Inventories urls diff --git a/kfet/views.py b/kfet/views.py index 7083d489..b5ade6ae 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -18,7 +18,7 @@ from django.contrib.auth.models import User, Permission, Group from django.http import HttpResponse, JsonResponse, Http404 from django.forms import modelformset_factory, formset_factory from django.db import IntegrityError, transaction -from django.db.models import F, Sum, Prefetch, Count, Func +from django.db.models import Q, F, Sum, Prefetch, Count, Func from django.db.models.functions import Coalesce from django.utils import timezone from django.utils.crypto import get_random_string @@ -26,7 +26,7 @@ from gestioncof.models import CofProfile, Clipper from kfet.decorators import teamkfet_required from kfet.models import (Account, Checkout, Article, Settings, AccountNegative, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, - InventoryArticle, Order, OrderArticle) + InventoryArticle, Order, OrderArticle, TransferGroup, Transfer) from kfet.forms import * from collections import defaultdict from kfet import consumers @@ -1097,26 +1097,47 @@ def kpsul_perform_operations(request): @teamkfet_required def kpsul_cancel_operations(request): # Pour la réponse - data = { 'canceled': [], 'warnings': {}, 'errors': {}} + 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('operations[]', [])))) + opes_post = set(map(lambda s: int(s.split()[1]), + filter(lambda s: s.split()[0] == 'ope', + request.POST.getlist('operations[]', [])))) + transfers_post = \ + set(map(lambda s: int(s.split()[1]), + filter(lambda s: s.split()[0] == 'transfer', + request.POST.getlist('operations[]', [])))) 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 + + transfers_all = ( + Transfer.objects + .select_related('group', 'from_acc', 'from_acc__negative', + 'to_acc', 'to_acc__negative') + .filter(pk__in=transfers_post)) + transfers_pk = [transfer.pk for transfer in transfers_all] + transfers_notexisting = [transfer for transfer in transfers_post + if transfer not in transfers_pk] + + if transfers_notexisting or opes_notexisting: + if transfers_notexisting: + data['errors']['transfers_notexisting'] = transfers_notexisting + 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 + already_canceled = {} # Opération/Transfert déjà annulé + opes = [] # Pas déjà annulée + transfers = [] required_perms = set() stop_all = False cancel_duration = Settings.CANCEL_DURATION() @@ -1127,7 +1148,7 @@ def kpsul_cancel_operations(request): 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) + already_canceled['opes'].append(ope.pk) else: opes.append(ope.pk) # Si opé il y a plus de CANCEL_DURATION, permission requise @@ -1161,7 +1182,7 @@ def kpsul_cancel_operations(request): if not last_statement or last_statement.at < ope.group.at: if ope.type == Operation.PURCHASE: if ope.group.on_acc.is_cash: - to_checkouts_balances[ope.group.checkout] -= - ope.amount + to_checkouts_balances[ope.group.checkout] -= -ope.amount else: to_checkouts_balances[ope.group.checkout] -= ope.amount @@ -1174,22 +1195,38 @@ def kpsul_cancel_operations(request): # est recalculé automatiquement if ope.article and ope.article_nb: last_stock = (InventoryArticle.objects - .select_related('inventory') - .filter(article=ope.article) - .order_by('inventory__at') - .last()) + .select_related('inventory') + .filter(article=ope.article) + .order_by('inventory__at') + .last()) if not last_stock or last_stock.inventory.at < ope.group.at: to_articles_stocks[ope.article] += ope.article_nb - if not opes: - data['warnings']['already_canceled'] = opes_already_canceled + for transfer in transfers_all: + if transfer.canceled_at: + # Transfert déjà annulé, va pour un warning en Response + already_canceled['transfers'].append(transfer.pk) + else: + transfers.append(transfer.pk) + # Si transfer il y a plus de CANCEL_DURATION, permission requise + if transfer.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 + to_accounts_balances[transfer.from_acc] += transfer.amount + to_accounts_balances[transfer.to_acc] += -transfer.amount + + if not opes and not transfers: + data['warnings']['already_canceled'] = already_canceled return JsonResponse(data) negative_accounts = [] # Checking permissions or stop for account in to_accounts_balances: (perms, stop) = account.perms_to_perform_operation( - amount = to_accounts_balances[account]) + amount=to_accounts_balances[account]) required_perms |= perms stop_all = stop_all or stop if stop: @@ -1209,25 +1246,31 @@ def kpsul_cancel_operations(request): with transaction.atomic(): (Operation.objects.filter(pk__in=opes) .update(canceled_by=canceled_by, canceled_at=canceled_at)) + + (Transfer.objects.filter(pk__in=transfers) + .update(canceled_by=canceled_by, canceled_at=canceled_at)) + for account in to_accounts_balances: Account.objects.filter(pk=account.pk).update( - balance = F('balance') + to_accounts_balances[account]) + 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]) + 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]) + 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]) + stock=F('stock') + to_articles_stocks[article]) # Websocket data - websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] } + websocket_data = {'opegroups': [], 'opes': [], + 'checkouts': [], 'articles': []} # Need refresh from db cause we used update on querysets - opegroups_pk = [ opegroup.pk for opegroup in to_groups_amounts ] + opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts] opegroups = (OperationGroup.objects - .values('id','amount','is_cof').filter(pk__in=opegroups_pk)) + .values('id', 'amount', 'is_cof') + .filter(pk__in=opegroups_pk)) for opegroup in opegroups: websocket_data['opegroups'].append({ 'cancellation': True, @@ -1244,15 +1287,16 @@ def kpsul_cancel_operations(request): 'canceled_at': canceled_at, }) # Need refresh from db cause we used update on querysets - checkouts_pk = [ checkout.pk for checkout in to_checkouts_balances] + checkouts_pk = [checkout.pk for checkout in to_checkouts_balances] checkouts = (Checkout.objects - .values('id', 'balance').filter(pk__in=checkouts_pk)) + .values('id', 'balance') + .filter(pk__in=checkouts_pk)) for checkout in checkouts: websocket_data['checkouts'].append({ 'id': checkout['id'], 'balance': checkout['balance']}) # Need refresh from db cause we used update on querysets - articles_pk = [ article.pk for articles in to_articles_stocks] + articles_pk = [article.pk for articles in to_articles_stocks] articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk) for article in articles: websocket_data['articles'].append({ @@ -1260,9 +1304,10 @@ def kpsul_cancel_operations(request): 'stock': article['stock']}) consumers.KPsul.group_send('kfet.kpsul', websocket_data) - data['canceled'] = opes - if opes_already_canceled: - data['warnings']['already_canceled'] = opes_already_canceled + data['canceled']['opes'] = opes + data['canceled']['transfers'] = transfers + if already_canceled: + data['warnings']['already_canceled'] = already_canceled return JsonResponse(data) @login_required @@ -1270,45 +1315,77 @@ def history_json(request): # Récupération des paramètres from_date = request.POST.get('from', None) to_date = request.POST.get('to', None) - limit = request.POST.get('limit', None); checkouts = request.POST.getlist('checkouts[]', None) accounts = request.POST.getlist('accounts[]', None) + transfers_only = request.POST.get('transfersonly', None) # Construction de la requête (sur les opérations) pour le prefetch - queryset_prefetch = Operation.objects.select_related( + ope_queryset_prefetch = Operation.objects.select_related( 'canceled_by__trigramme', 'addcost_for__trigramme', 'article__name') + ope_prefetch = Prefetch('opes', + queryset = ope_queryset_prefetch) + + transfer_queryset_prefetch = Transfer.objects.select_related( + 'from_acc__trigramme', 'to_acc__trigramme', + 'from_acc__id', 'to_acc__id', + 'canceled_by__trigramme') + + if accounts: + transfer_queryset_prefetch = transfer_queryset_prefetch.filter( + Q(from_acc__id__in=accounts) | + Q(to_acc__id__in=accounts)) + + if not request.user.has_perm('kfet.is_team'): + acc = request.user.profile.account_kfet + transfer_queryset_prefetch = transfer_queryset_prefetch.filter( + Q(from_acc=acc) | Q(to_acc=acc)) + + transfer_prefetch = Prefetch('transfers', + queryset=transfer_queryset_prefetch, + to_attr='filtered_transfers') + # Construction de la requête principale opegroups = (OperationGroup.objects - .prefetch_related(Prefetch('opes', queryset = queryset_prefetch)) + .prefetch_related(ope_prefetch) .select_related('on_acc__trigramme', 'valid_by__trigramme') .order_by('at') ) + + transfergroups = ( + TransferGroup.objects + .prefetch_related(transfer_prefetch) + .select_related('valid_by__trigramme') + .order_by('at')) + # Application des filtres if from_date: opegroups = opegroups.filter(at__gte=from_date) + transfergroups = transfergroups.filter(at__gte=from_date) if to_date: opegroups = opegroups.filter(at__lt=to_date) + transfergroups = transfergroups.filter(at__lt=to_date) if checkouts: opegroups = opegroups.filter(checkout_id__in=checkouts) + transfergroups = TransferGroup.objects.none() + if transfers_only: + opegroups = OperationGroup.objects.none() if accounts: opegroups = opegroups.filter(on_acc_id__in=accounts) # Un non-membre de l'équipe n'a que accès à son historique if not request.user.has_perm('kfet.is_team'): opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet) - if limit: - opegroups = opegroups[:limit] # Construction de la réponse opegroups_list = [] for opegroup in opegroups: opegroup_dict = { + 'type' : 'opegroup', 'id' : opegroup.id, 'amount' : opegroup.amount, 'at' : opegroup.at, - 'checkout_id': opegroup.checkout_id, 'is_cof' : opegroup.is_cof, 'comment' : opegroup.comment, 'opes' : [], @@ -1337,7 +1414,41 @@ def history_json(request): ope.canceled_by and ope.canceled_by.trigramme or None) opegroup_dict['opes'].append(ope_dict) opegroups_list.append(opegroup_dict) - return JsonResponse({ 'opegroups': opegroups_list }) + + for transfergroup in transfergroups: + if transfergroup.filtered_transfers: + transfergroup_dict = { + 'type': 'transfergroup', + 'id': transfergroup.id, + 'at': transfergroup.at, + 'comment': transfergroup.comment, + 'opes': [], + } + if request.user.has_perm('kfet.is_team'): + transfergroup_dict['valid_by__trigramme'] = ( + transfergroup.valid_by + and transfergroup.valid_by.trigramme + or None) + + for transfer in transfergroup.filtered_transfers: + transfer_dict = { + 'id': transfer.id, + 'amount': transfer.amount, + 'canceled_at': transfer.canceled_at, + 'from_acc': transfer.from_acc.trigramme, + 'to_acc': transfer.to_acc.trigramme, + } + if request.user.has_perm('kfet.is_team'): + transfer_dict['canceled_by__trigramme'] = ( + transfer.canceled_by + and transfer.canceled_by.trigramme + or None) + transfergroup_dict['opes'].append(transfer_dict) + opegroups_list.append(transfergroup_dict) + + opegroups_list.sort(key=lambda group: group['at']) + + return JsonResponse({'opegroups': opegroups_list}) @teamkfet_required def kpsul_articles_data(request): @@ -1483,104 +1594,6 @@ def perform_transfers(request): return JsonResponse(data) -@teamkfet_required -def cancel_transfers(request): - # Pour la réponse - data = { 'canceled': [], 'warnings': {}, 'errors': {}} - - # Checking if BAD REQUEST (transfers_pk not int or not existing) - try: - # Set pour virer les doublons - transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', [])))) - except ValueError: - return JsonResponse(data, status=400) - transfers_all = ( - Transfer.objects - .select_related('group', 'from_acc', 'from_acc__negative', - 'to_acc', 'to_acc__negative') - .filter(pk__in=transfers_post)) - transfers_pk = [ transfer.pk for transfer in transfers_all ] - transfers_notexisting = [ transfer for transfer in transfers_post - if transfer not in transfers_pk ] - if transfers_notexisting: - data['errors']['transfers_notexisting'] = transfers_notexisting - return JsonResponse(data, status=400) - - transfers_already_canceled = [] # Déjà annulée - transfers = [] # 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 - for transfer in transfers_all: - if transfer.canceled_at: - # Transfert déjà annulé, va pour un warning en Response - transfers_already_canceled.append(transfer.pk) - else: - transfers.append(transfer.pk) - # Si transfer il y a plus de CANCEL_DURATION, permission requise - if transfer.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 - to_accounts_balances[transfer.from_acc] += transfer.amount - to_accounts_balances[transfer.to_acc] += -transfer.amount - - if not transfers: - data['warnings']['already_canceled'] = transfers_already_canceled - return JsonResponse(data) - - negative_accounts = [] - # Checking permissions or stop - for account in to_accounts_balances: - (perms, stop) = account.perms_to_perform_operation( - amount = to_accounts_balances[account]) - required_perms |= perms - stop_all = stop_all or stop - if stop: - negative_accounts.append(account.trigramme) - - print(required_perms) - print(request.user.get_all_permissions()) - - 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'] = negative_accounts - return JsonResponse(data, status=403) - - canceled_by = required_perms and request.user.profile.account_kfet or None - canceled_at = timezone.now() - - with transaction.atomic(): - (Transfer.objects.filter(pk__in=transfers) - .update(canceled_by=canceled_by, canceled_at=canceled_at)) - - for account in to_accounts_balances: - Account.objects.filter(pk=account.pk).update( - balance = F('balance') + to_accounts_balances[account]) - account.refresh_from_db() - if account.balance < 0: - if hasattr(account, 'negative'): - if not account.negative.start: - account.negative.start = timezone.now() - account.negative.save() - else: - negative = AccountNegative( - account = account, start = timezone.now()) - negative.save() - elif (hasattr(account, 'negative') - and not account.negative.balance_offset): - account.negative.delete() - - data['canceled'] = transfers - if transfers_already_canceled: - data['warnings']['already_canceled'] = transfers_already_canceled - return JsonResponse(data) class InventoryList(ListView): queryset = (Inventory.objects