diff --git a/kfet/__init__.py b/kfet/__init__.py index e69de29b..5d6c8f97 100644 --- a/kfet/__init__.py +++ b/kfet/__init__.py @@ -0,0 +1 @@ +default_app_config = 'kfet.apps.KFetConfig' diff --git a/kfet/apps.py b/kfet/apps.py new file mode 100644 index 00000000..29f9f98e --- /dev/null +++ b/kfet/apps.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from builtins import * + +from django.apps import AppConfig + +class KFetConfig(AppConfig): + name = 'kfet' + verbose_name = "Application K-Fêt" + + def ready(self): + import kfet.signals diff --git a/kfet/backends.py b/kfet/backends.py index 62b2d820..3729f1bd 100644 --- a/kfet/backends.py +++ b/kfet/backends.py @@ -18,7 +18,7 @@ class KFetBackend(object): return None try: - password_sha256 = hashlib.sha256(password.encode()).hexdigest() + password_sha256 = hashlib.sha256(password.encode('utf-8')).hexdigest() account = Account.objects.get(password=password_sha256) user = account.cofprofile.user except Account.DoesNotExist: diff --git a/kfet/routing.py b/kfet/routing.py index e7bcca55..9c816c92 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -8,7 +8,7 @@ from channels.routing import route, route_class from kfet import consumers channel_routing = [ - route_class(consumers.KPsul, path=r"^/ws/k-fet/k-psul/$"), + route_class(consumers.KPsul, path=r"^/gestion/ws/k-fet/k-psul/$"), #route("websocket.connect", ws_kpsul_history_connect), #route('websocket.receive', ws_message) ] diff --git a/kfet/signals.py b/kfet/signals.py new file mode 100644 index 00000000..3dd4d677 --- /dev/null +++ b/kfet/signals.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from builtins import * + +from django.contrib import messages +from django.contrib.auth.signals import user_logged_in +from django.core.urlresolvers import reverse +from django.dispatch import receiver + +@receiver(user_logged_in) +def messages_on_login(sender, request, user, **kwargs): + if (not user.username == 'kfet_genericteam' + and user.has_perm('kfet.is_team')): + messages.info(request, 'Connexion en utilisateur partagé ?' % reverse('kfet.login.genericteam'), extra_tags='safe') diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index c6ee9ff6..f3e9bf8c 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -263,3 +263,81 @@ textarea { display:block; padding:5px 20px; } + +/* + * Messages + */ + +.messages .alert { + padding:10px 15px; + margin:0; + border:0; + border-radius:0; +} + +.messages .alert-dismissible { + padding-right:35px; +} + +.messages .alert .close { + top:0; + right:0; +} + +.messages .alert-info { + color:inherit; + background-color:#ccc; +} + +.messages .alert-error { + color:inherit; + background-color:rgba(200,16,46,0.2); +} + +.messages .alert-success { + color:#333; +} + +/* + * Help + */ + +.help { + display:none; + position:fixed; + top:50px; + left:0; + right:0; + bottom:0; + overflow:auto; + background:rgba(51,51,51,0.3); + z-index:500; +} + +.help-box { + margin-top:30px; + padding-top:1px; + padding-bottom:15px; + background:rgba(51,51,51,0.7); + color:#fff; +} + +@media (max-width:768px) { + .help-box { + margin:20px 15px; + } +} + +.help h2 { + padding:0 15px 20px; + border-bottom:1px solid #999; + text-align:center; +} + +.help .row > div { + padding-right:0; +} + +.help h4 { + margin:15px 0; +} diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index b15f6de8..9fd53604 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -319,6 +319,11 @@ input[type=number]::-webkit-outer-spin-button { padding-left:20px; } +#articles_data .article:hover { + background:rgba(200,16,46,0.3); + cursor:pointer; +} + /* Second part - Left - bottom */ .kpsul_middle_left_bottom { diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 7aa1d963..dbfba0b2 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -66,8 +66,11 @@ function getErrorsHtml(data) { content += ''; } if ('negative' in data['errors']) { - var url_base = "{% url 'kfet.account.update' LIQ}"; - url_base = base_url(0, url_base.length-8); + if (window.location.pathname.startsWith('/gestion/')) { + var url_base = '/gestion/k-fet/accounts/'; + } else { + var url_base = '/k-fet/accounts/'; + } for (var i=0; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; } @@ -110,4 +113,3 @@ function requestAuth(data, callback, focus_next = null) { } }); } - diff --git a/kfet/templates/kfet/account_create_autocomplete.html b/kfet/templates/kfet/account_create_autocomplete.html index b99abecf..1185c3a8 100644 --- a/kfet/templates/kfet/account_create_autocomplete.html +++ b/kfet/templates/kfet/account_create_autocomplete.html @@ -38,6 +38,7 @@
  • {{ clipper|highlight_clipper:q }} +
  • {% endfor %} {% endif %} diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 77fdf118..5f77b8f0 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -59,7 +59,7 @@ {{ neg.account.name }} {{ neg.account.balance|floatformat:2 }}€ - {% if neg.account.balance_offset %} + {% if neg.balance_offset %} {{ neg.account.real_balance|floatformat:2 }}€ {% endif %} diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index c1db0c26..173a5fb7 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -40,5 +40,13 @@ {% block content %}{% endblock %} {% include "kfet/base_footer.html" %} +
    +
    +
    +

    Aide

    + {% block help %}{% endblock %} +
    +
    +
    diff --git a/kfet/templates/kfet/base_messages.html b/kfet/templates/kfet/base_messages.html index d2bb5061..440b8c10 100644 --- a/kfet/templates/kfet/base_messages.html +++ b/kfet/templates/kfet/base_messages.html @@ -1,7 +1,16 @@ {% if messages %} - +
    + {% for message in messages %} +
    +
    + + {% if 'safe' in message.tags %} + {{ message|safe }} + {% else %} + {{ message }} + {% endif %} +
    +
    + {% endfor %} +
    {% endif %} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 1694d57f..ca401ccb 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -19,8 +19,59 @@ {% block content-header %}{% endblock %} +{% block help %} + +
    +
    +
    +

    Opérations

    +
    +
    F3
    +
    Charge
    +
    +
    +
    Shift + F3
    +
    Retrait
    +
    +
    +
    F8
    +
    Edition
    +
    +
    +
    +
    +
    +

    Général

    +
    +
    F1
    +
    Reset
    +
    +
    +
    F2
    +
    Reset compte
    +
    +
    +
    Shift + F2
    +
    Reset panier
    +
    +
    +
    F9
    +
    Majoration
    +
    +
    +
    F10
    +
    Hard reset
    +
    +
    +
    +
    + +{% endblock %} + {% block content %} +{% include 'kfet/base_messages.html' %} +
    @@ -69,7 +120,7 @@
    - +
    @@ -198,8 +249,9 @@ $(document).ready(function() { } if (account_data['id'] == 0) { var trigramme = triInput.val().toUpperCase(); + var url_base = '{% url 'kfet.account.create' %}' if (isValidTrigramme(trigramme)) { - buttons += ''; + buttons += ''; } } account_container.find('.buttons').html(buttons); @@ -504,17 +556,17 @@ $(document).ready(function() { function addArticle(article) { var article_html = $(article_default_html); - article_html.attr('data-article', article['id']); - article_html.attr('data-category', article['category_id']); + article_html.attr('id', 'data-article-'+article['id']); + article_html.addClass('data-category-'+article['category_id']); for (var elem in article) { article_html.find('.'+elem).text(article[elem]) } article_html.find('.price').text(amountToUKF(article['price'], false)); var category_html = articles_container - .find('.category[data-category='+article['category_id']+']'); + .find('#data-category-'+article['category_id']); if (category_html.length == 0) { category_html = $(article_category_default_html); - category_html.attr('data-category', article['category_id']); + category_html.attr('id', 'data-category-'+article['category_id']); category_html.find('td').text(article['category__name']); var added = false; articles_container.find('.category').each(function() { @@ -526,16 +578,14 @@ $(document).ready(function() { }); if (!added) articles_container.append(category_html); } - var added = false; + var $after = articles_container.find('#data-category-'+article['category_id']); articles_container - .find('.article[data-category='+article['category_id']+']').each(function() { - if (article['name'].toLowerCase < $('.name', this).text().toLowerCase()) { - $(this).before(article_html); - added = true; + .find('.article.data-category-'+article['category_id']).each(function() { + if (article['name'].toLowerCase < $('.name', this).text().toLowerCase()) return false; - } + $after = $(this); }); - if (!added) articles_container.find('.category[data-category='+article['category_id']+']').after(article_html); + $after.after(article_html); // Pour l'autocomplétion articlesList.push([article['name'],article['id'],article['category_id'],article['price']]); } @@ -597,17 +647,17 @@ $(document).ready(function() { var categories_to_display = []; for (var i=0; i -1) { - articles_container.find('[data-article='+articlesList[i][1]+']').show(); + articles_container.find('#data-article-'+articlesList[i][1]).show(); if (categories_to_display.indexOf(articlesList[i][2]) == -1) categories_to_display.push(articlesList[i][2]); } else { - articles_container.find('[data-article='+articlesList[i][1]+']').hide(); + articles_container.find('#data-article-'+articlesList[i][1]).hide(); } } articles_container.find('.category').hide(); for (var i=0; i 0 && nb <= 24; } @@ -1088,7 +1156,9 @@ $(document).ready(function() { websocket_msg_default = {'opegroups':[],'opes':[],'checkouts':[],'articles':[]} var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; - socket = new ReconnectingWebSocket(websocket_protocol+"://" + window.location.host + "/ws/k-fet/k-psul/"); + var location_host = window.location.host; + var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host; + socket = new ReconnectingWebSocket(websocket_protocol+"://" + location_url + "/ws/k-fet/k-psul/"); socket.onmessage = function(e) { data = $.extend({}, websocket_msg_default, JSON.parse(e.data)); @@ -1139,7 +1209,7 @@ $(document).ready(function() { function hardReset(give_tri_focus=true) { coolReset(give_tri_focus); - resetCheckout(); + checkoutInput.trigger('change'); resetArticles(); khistory.reset(); resetSettings(); @@ -1163,6 +1233,16 @@ $(document).ready(function() { $(document).on('keydown', function(e) { switch (e.keyCode) { + case 27: + // Escape - Hide help + $('.help').hide('fast'); + return false; + case 72: + if (e.ctrlKey) { + // Ctrl+H - Display help + $('.help').show('fast'); + } + return false; case 112: // F1 - Cool reset coolReset(); diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html index 1875a21e..cbdf0fe3 100644 --- a/kfet/templates/kfet/transfers.html +++ b/kfet/templates/kfet/transfers.html @@ -1,4 +1,13 @@ {% extends 'kfet/base.html' %} +{% load staticfiles %} + +{% block extra_head %} + + + + + +{% endblock %} {% block title %}Transferts{% endblock %} {% block content-header-title %}Transferts{% endblock %} @@ -24,13 +33,13 @@

    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 }} @@ -44,4 +53,93 @@
    + + {% endblock %} diff --git a/kfet/urls.py b/kfet/urls.py index e0bacf9a..9b9ebf21 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -170,6 +170,8 @@ 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 736dbe8a..3f1e1f4d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -219,7 +219,7 @@ def account_form_set_readonly_fields(user_form, cof_form): def get_account_create_forms(request=None, username=None, login_clipper=None): user = None clipper = None - if login_clipper and not username: + if login_clipper and (login_clipper == username or not username): # à partir d'un clipper # le user associé à ce clipper ne devrait pas encore exister clipper = get_object_or_404(Clipper, username = login_clipper) @@ -399,7 +399,7 @@ def account_update(request, trigramme): if (request.user.has_perm('kfet.change_account_password') and pwd_form.is_valid()): pwd = pwd_form.cleaned_data['pwd1'] - pwd_sha256 = hashlib.sha256(pwd.encode()).hexdigest() + pwd_sha256 = hashlib.sha256(pwd.encode('utf-8')).hexdigest() Account.objects.filter(pk=account.pk).update( password = pwd_sha256) messages.success(request, 'Mot de passe mis à jour') @@ -796,7 +796,15 @@ def kpsul(request): data = {} data['operationgroup_form'] = KPsulOperationGroupForm() data['trigramme_form'] = KPsulAccountForm() - data['checkout_form'] = KPsulCheckoutForm() + initial = {} + try: + checkout = Checkout.objects.filter( + is_protected=False, valid_from__lte=timezone.now(), + valid_to__gte=timezone.now()).get() + initial['checkout'] = checkout + except (Checkout.DoesNotExist, Checkout.MultipleObjectsReturned): + pass + data['checkout_form'] = KPsulCheckoutForm(initial=initial) operation_formset = KPsulOperationFormSet(queryset=Operation.objects.none()) data['operation_formset'] = operation_formset return render(request, 'kfet/kpsul.html', data) @@ -827,28 +835,27 @@ def kpsul_checkout_data(request): pk = request.POST.get('pk', 0) if not pk: pk = 0 - try: - data = (Checkout.objects - .annotate( - last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'), - last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'), - last_statement_by_trigramme=F('statements__by__trigramme'), - last_statement_balance=F('statements__balance_new'), - last_statement_at=F('statements__at')) - .values( - 'id', 'name', 'balance', 'valid_from', 'valid_to', - 'last_statement_balance', 'last_statement_at', - 'last_statement_by_trigramme', 'last_statement_by_last_name', - 'last_statement_by_first_name') - .select_related( - 'statements' - 'statements__by', - 'statements__by__cofprofile__user') - .filter(pk=pk) - .order_by('statements__at') - .last()) - except Checkout.DoesNotExist: - raise http404 + data = (Checkout.objects + .annotate( + last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'), + last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'), + last_statement_by_trigramme=F('statements__by__trigramme'), + last_statement_balance=F('statements__balance_new'), + last_statement_at=F('statements__at')) + .values( + 'id', 'name', 'balance', 'valid_from', 'valid_to', + 'last_statement_balance', 'last_statement_at', + 'last_statement_by_trigramme', 'last_statement_by_last_name', + 'last_statement_by_first_name') + .select_related( + 'statements' + 'statements__by', + 'statements__by__cofprofile__user') + .filter(pk=pk) + .order_by('statements__at') + .last()) + if data is None: + raise Http404 return JsonResponse(data) @teamkfet_required @@ -1402,7 +1409,7 @@ def perform_transfers(request): transfers = transfer_formset.save(commit = False) # Initializing vars - required_perms = set('kfet.add_transfer') # Required perms to perform all transfers + required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers to_accounts_balances = defaultdict(lambda:0) # For balances of accounts for transfer in transfers: @@ -1468,6 +1475,105 @@ 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 .select_related('by', 'order')