# -*- coding: utf-8 -*-

from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
from builtins import *

from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.cache import cache
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.core.urlresolvers import reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, permission_required
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.functions import Coalesce
from django.utils import timezone
from django.utils.crypto import get_random_string
from gestioncof.models import CofProfile
from kfet.decorators import teamkfet_required
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
    CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
    InventoryArticle, Order, OrderArticle)
from kfet.forms import *
from collections import defaultdict
from kfet import consumers
from datetime import timedelta
import django_cas_ng
import hashlib
import heapq
import statistics

@login_required
def home(request):
    return render(request, "kfet/base.html")

@teamkfet_required
def login_genericteam(request):
    # Check si besoin de déconnecter l'utilisateur de CAS
    profile, _ = CofProfile.objects.get_or_create(user=request.user)
    need_cas_logout = False
    if profile.login_clipper:
        need_cas_logout = True
        # Récupèration de la vue de déconnexion de CAS
        # Ici, car request sera modifié après
        logout_cas = django_cas_ng.views.logout(request)

    # Authentification du compte générique
    token = GenericTeamToken.objects.create(token=get_random_string(50))
    user = authenticate(username="kfet_genericteam", token=token.token)
    login(request, user)

    if need_cas_logout:
        # Vue de déconnexion de CAS
        return logout_cas

    return render(request, "kfet/login_genericteam.html")

def put_cleaned_data_in_dict(dict, form):
    for field in form.cleaned_data:
        dict[field] = form.cleaned_data[field]

# -----
# Account views
# -----

# Account - General

@login_required
@teamkfet_required
def account(request):
    accounts = Account.objects.select_related('cofprofile__user').order_by('trigramme')
    return render(request, "kfet/account.html", { 'accounts' : accounts })

@login_required
@teamkfet_required
def account_is_validandfree_ajax(request):
    if not request.GET.get("trigramme", ''):
        raise Http404
    trigramme = request.GET.get("trigramme")
    data = Account.is_validandfree(trigramme)
    return JsonResponse(data)

# Account - Create

@login_required
@teamkfet_required
def account_create_special(request):

    # Enregistrement
    if request.method == "POST":
        trigramme_form = AccountTriForm(request.POST, initial={'balance':0})
        balance_form = AccountBalanceForm(request.POST)

        # Peuplement des forms
        username = request.POST.get('username')
        login_clipper = request.POST.get('login_clipper')

        forms = get_account_create_forms(
                request, username=username, login_clipper=login_clipper)

        account_form = forms['account_form']
        cof_form = forms['cof_form']
        user_form = forms['user_form']

        if all((user_form.is_valid(), cof_form.is_valid(),
                trigramme_form.is_valid(), account_form.is_valid(),
                balance_form.is_valid())):
            # Checking permission
            if not request.user.has_perm('kfet.special_add_account'):
                messages.error(request, 'Permission refusée')
            else:
                data = {}
                # Fill data for Account.save()
                put_cleaned_data_in_dict(data, user_form)
                put_cleaned_data_in_dict(data, cof_form)

                try:
                    account = trigramme_form.save(data = data)
                    account_form = AccountNoTriForm(request.POST, instance=account)
                    account_form.save()
                    balance_form = AccountBalanceForm(request.POST, instance=account)
                    balance_form.save()
                    amount = balance_form.cleaned_data['balance']
                    checkout = Checkout.objects.get(name='Initial')
                    is_cof = account.is_cof
                    opegroup = OperationGroup.objects.create(
                            on_acc=account,
                            checkout=checkout,
                            amount = amount,
                            is_cof = account.is_cof)
                    ope = Operation.objects.create(
                            group = opegroup,
                            type = Operation.INITIAL,
                            amount = amount,
                            is_checkout = False)
                    messages.success(request, 'Compte créé : %s' % account.trigramme)
                    return redirect('kfet.account.create')
                except Account.UserHasAccount as e:
                    messages.error(request, \
                        "Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
    else:
        initial = { 'trigramme': request.GET.get('trigramme', '') }
        trigramme_form = AccountTriForm(initial = initial)
        balance_form = AccountBalanceForm(initial = {'balance': 0})
        account_form = None
        cof_form = None
        user_form = None

    return render(request, "kfet/account_create_special.html", {
        'trigramme_form': trigramme_form,
        'account_form': account_form,
        'cof_form': cof_form,
        'user_form': user_form,
        'balance_form': balance_form,
    })


# Account - Create

@login_required
@teamkfet_required
def account_create(request):

    # Enregistrement
    if request.method == "POST":
        trigramme_form = AccountTriForm(request.POST)

        # Peuplement des forms
        username = request.POST.get('username')
        login_clipper = request.POST.get('login_clipper')

        forms = get_account_create_forms(
                request, username=username, login_clipper=login_clipper)

        account_form = forms['account_form']
        cof_form = forms['cof_form']
        user_form = forms['user_form']

        if all((user_form.is_valid(), cof_form.is_valid(),
                trigramme_form.is_valid(), account_form.is_valid())):
            # Checking permission
            if not request.user.has_perm('kfet.add_account'):
                messages.error(request, 'Permission refusée')
            else:
                data = {}
                # Fill data for Account.save()
                put_cleaned_data_in_dict(data, user_form)
                put_cleaned_data_in_dict(data, cof_form)

                try:
                    account = trigramme_form.save(data = data)
                    account_form = AccountNoTriForm(request.POST, instance=account)
                    account_form.save()
                    messages.success(request, 'Compte créé : %s' % account.trigramme)
                    return redirect('kfet.account.create')
                except Account.UserHasAccount as e:
                    messages.error(request, \
                        "Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
    else:
        initial = { 'trigramme': request.GET.get('trigramme', '') }
        trigramme_form = AccountTriForm(initial = initial)
        account_form = None
        cof_form = None
        user_form = None

    return render(request, "kfet/account_create.html", {
        'trigramme_form': trigramme_form,
        'account_form': account_form,
        'cof_form': cof_form,
        'user_form': user_form,
    })

def account_form_set_readonly_fields(user_form, cof_form):
    user_form.fields['username'].widget.attrs['readonly'] = True
    cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
    cof_form.fields['is_cof'].widget.attrs['disabled'] = True

def get_account_create_forms(request=None, username=None, login_clipper=None,
                             fullname=None):
    user = None
    clipper = False
    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 = True
        try:
            # Vérification que clipper ne soit pas déjà dans User
            user = User.objects.get(username=login_clipper)
            # Ici, on nous a menti, le user existe déjà
            username = user.username
            clipper = False
        except User.DoesNotExist:
            # Clipper (sans user déjà existant)

            # UserForm - Prefill
            user_initial = {
                    'username' : login_clipper,
                    'email'    : "%s@clipper.ens.fr" % login_clipper}
            if fullname:
                # Prefill du nom et prénom
                names = fullname.split()
                # Le premier, c'est le prénom
                user_initial['first_name'] = names[0]
                if len(names) > 1:
                    # Si d'autres noms -> tous dans le nom de famille
                    user_initial['last_name'] = " ".join(names[1:])
            # CofForm - Prefill
            cof_initial = { 'login_clipper': login_clipper }

            # Form créations
            if request:
                user_form = UserForm(request.POST, initial=user_initial, from_clipper=True)
                cof_form  = CofForm(request.POST, initial=cof_initial)
            else:
                user_form = UserForm(initial=user_initial, from_clipper=True)
                cof_form  = CofForm(initial=cof_initial)

            # Protection (read-only) des champs username et login_clipper
            account_form_set_readonly_fields(user_form, cof_form)
    if username and not clipper:
        try:
            user = User.objects.get(username=username)
            # le user existe déjà
            # récupération du profil cof
            (cof, _) = CofProfile.objects.get_or_create(user=user)
            # UserForm + CofForm - Création à partir des instances existantes
            if request:
                user_form = UserForm(request.POST, instance = user)
                cof_form  = CofForm(request.POST, instance = cof)
            else:
                user_form = UserForm(instance=user)
                cof_form  = CofForm(instance=cof)
            # Protection (read-only) des champs username, login_clipper et is_cof
            account_form_set_readonly_fields(user_form, cof_form)
        except User.DoesNotExist:
            # le username donnée n'existe pas -> Création depuis rien
            # (éventuellement en cours avec erreurs précédemment)
            pass
    if not user and not clipper:
        # connaît pas du tout, faut tout remplir
        if request:
            user_form = UserForm(request.POST)
            cof_form  = CofForm(request.POST)
        else:
            user_form = UserForm()
            cof_form  = CofForm()
        # mais on laisse le username en écriture
        cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
        cof_form.fields['is_cof'].widget.attrs['disabled'] = True

    if request:
        account_form = AccountNoTriForm(request.POST)
    else:
        account_form = AccountNoTriForm()

    return {
        'account_form': account_form,
        'cof_form': cof_form,
        'user_form': user_form,
    }


@login_required
@teamkfet_required
def account_create_ajax(request, username=None, login_clipper=None,
                        fullname=None):
    forms = get_account_create_forms(
        request=None, username=username, login_clipper=login_clipper,
        fullname=fullname)
    return render(request, "kfet/account_create_form.html", {
            'account_form' : forms['account_form'],
            'cof_form'     : forms['cof_form'],
            'user_form'    : forms['user_form'],
            })

# Account - Read

@login_required
def account_read(request, trigramme):
    try:
        account = Account.objects.select_related('negative').get(trigramme=trigramme)
    except Account.DoesNotExist:
        raise Http404

    # Checking permissions
    if not request.user.has_perm('kfet.is_team') \
            and request.user != account.user:
        raise PermissionDenied

    addcosts = (OperationGroup.objects
            .filter(opes__addcost_for=account,opes__canceled_at=None)
            .extra({'date':"date(at)"})
            .values('date')
            .annotate(sum_addcosts=Sum('opes__addcost_amount'))
            .order_by('-date'))

    return render(request, "kfet/account_read.html", {
        'account' : account,
        'addcosts': addcosts,
        'settings': { 'subvention_cof': Settings.SUBVENTION_COF() },
    })

# Account - Update

@login_required
def account_update(request, trigramme):
    account = get_object_or_404(Account, trigramme=trigramme)

    # Checking permissions
    if not request.user.has_perm('kfet.is_team') \
            and request.user != account.user:
        raise PermissionDenied

    if request.user.has_perm('kfet.is_team'):
        user_form    = UserRestrictTeamForm(instance=account.user)
        group_form   = UserGroupForm(instance=account.user)
        account_form = AccountForm(instance=account)
        cof_form     = CofRestrictForm(instance=account.cofprofile)
        pwd_form     = AccountPwdForm()
        if account.balance < 0 and not hasattr(account, 'negative'):
            AccountNegative.objects.create(account=account, start=timezone.now())
            account.refresh_from_db()
        if hasattr(account, 'negative'):
            negative_form = AccountNegativeForm(instance=account.negative)
        else:
            negative_form = None
    else:
        user_form    = UserRestrictForm(instance=account.user)
        account_form = AccountRestrictForm(instance=account)
        cof_form     = None
        group_form   = None
        negative_form = None
        pwd_form     = None

    if request.method == "POST":
        # Update attempt
        success      = False
        missing_perm = True

        if request.user.has_perm('kfet.is_team'):
            account_form = AccountForm(request.POST, instance=account)
            cof_form     = CofRestrictForm(request.POST, instance=account.cofprofile)
            user_form    = UserRestrictTeamForm(request.POST, instance=account.user)
            group_form   = UserGroupForm(request.POST, instance=account.user)
            pwd_form     = AccountPwdForm(request.POST)
            if hasattr(account, 'negative'):
                negative_form = AccountNegativeForm(request.POST, instance=account.negative)

            if (request.user.has_perm('kfet.change_account')
                    and account_form.is_valid() and cof_form.is_valid()
                    and user_form.is_valid()):
                missing_perm = False
                data = {}
                # Fill data for Account.save()
                put_cleaned_data_in_dict(data, user_form)
                put_cleaned_data_in_dict(data, cof_form)

                # Updating
                account_form.save(data = data)

                # Checking perm to update password
                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('utf-8')).hexdigest()
                    Account.objects.filter(pk=account.pk).update(
                            password = pwd_sha256)
                    messages.success(request, 'Mot de passe mis à jour')

                # Checking perm to manage perms
                if (request.user.has_perm('kfet.manage_perms')
                        and group_form.is_valid()):
                    group_form.save()

                # Checking perm to manage negative
                if hasattr(account, 'negative'):
                    balance_offset_old = 0
                    if account.negative.balance_offset:
                        balance_offset_old = account.negative.balance_offset
                if (hasattr(account, 'negative')
                        and request.user.has_perm('kfet.change_accountnegative')
                        and negative_form.is_valid()):
                    balance_offset_new = negative_form.cleaned_data['balance_offset']
                    if not balance_offset_new:
                        balance_offset_new = 0
                    balance_offset_diff = balance_offset_new - balance_offset_old
                    Account.objects.filter(pk=account.pk).update(
                        balance = F('balance') + balance_offset_diff)
                    negative_form.save()
                    if not balance_offset_new and Account.objects.get(pk=account.pk).balance >= 0:
                        AccountNegative.objects.get(account=account).delete()

                success = True
                messages.success(request,
                    'Informations du compte %s mises à jour' % account.trigramme)

        if request.user == account.user:
            missing_perm = False
            account.refresh_from_db()
            user_form = UserRestrictForm(request.POST, instance=account.user)
            account_form = AccountRestrictForm(request.POST, instance=account)

            if user_form.is_valid() and account_form.is_valid():
                user_form.save()
                account_form.save()
                success = True
                messages.success(request, 'Vos informations ont été mises à jour')

        if missing_perm:
            messages.error(request, 'Permission refusée')
        if success:
            return redirect('kfet.account.read', account.trigramme)
        else:
            messages.error(request, 'Informations non mises à jour. Corrigez les erreurs')

    return render(request, "kfet/account_update.html", {
        'account'      : account,
        'account_form' : account_form,
        'cof_form'     : cof_form,
        'user_form'    : user_form,
        'group_form'   : group_form,
        'negative_form': negative_form,
        'pwd_form'     : pwd_form,
    })

@permission_required('kfet.manage_perms')
def account_group(request):
    groups = (Group.objects
        .filter(name__icontains='K-Fêt')
        .prefetch_related('permissions', 'user_set__profile__account_kfet')
    )
    return render(request, 'kfet/account_group.html', { 'groups': groups })

class AccountGroupCreate(SuccessMessageMixin, CreateView):
    model = Group
    template_name = 'kfet/account_group_form.html'
    form_class = GroupForm
    success_message = 'Nouveau groupe : %(name)s'
    success_url = reverse_lazy('kfet.account.group')

class AccountGroupUpdate(UpdateView):
    queryset = Group.objects.filter(name__icontains='K-Fêt')
    template_name = 'kfet/account_group_form.html'
    form_class = GroupForm
    success_message = 'Groupe modifié : %(name)s'
    success_url = reverse_lazy('kfet.account.group')

class AccountNegativeList(ListView):
    queryset = (AccountNegative.objects
        .select_related('account', 'account__cofprofile__user')
        .exclude(account__trigramme='#13'))
    template_name = 'kfet/account_negative.html'
    context_object_name = 'negatives'

    def get_context_data(self, **kwargs):
        context = super(AccountNegativeList, self).get_context_data(**kwargs)
        context['settings'] = {
            'overdraft_amount': Settings.OVERDRAFT_AMOUNT(),
            'overdraft_duration': Settings.OVERDRAFT_DURATION(),
        }
        negs_sum = (AccountNegative.objects
            .exclude(account__trigramme='#13')
            .aggregate(
                bal    = Coalesce(Sum('account__balance'),0),
                offset = Coalesce(Sum('balance_offset'),0),
            )
        )
        context['negatives_sum'] = negs_sum['bal'] - negs_sum['offset']
        return context

# -----
# Checkout views
# -----

# Checkout - General

class CheckoutList(ListView):
    model         = Checkout
    template_name = 'kfet/checkout.html'
    context_object_name = 'checkouts'

# Checkout - Create

class CheckoutCreate(SuccessMessageMixin, CreateView):
    model           = Checkout
    template_name   = 'kfet/checkout_create.html'
    form_class      = CheckoutForm
    success_message = 'Nouvelle caisse : %(name)s'

    # Surcharge de la validation
    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.add_checkout'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)

        # Creating
        form.instance.created_by = self.request.user.profile.account_kfet
        checkout = form.save()

        # Création d'un relevé avec balance initiale
        CheckoutStatement.objects.create(
                checkout = checkout,
                by = self.request.user.profile.account_kfet,
                balance_old = checkout.balance,
                balance_new = checkout.balance,
                amount_taken = 0)

        return super(CheckoutCreate, self).form_valid(form)

# Checkout - Read

class CheckoutRead(DetailView):
    model = Checkout
    template_name = 'kfet/checkout_read.html'
    context_object_name = 'checkout'

    def get_context_data(self, **kwargs):
        context = super(CheckoutRead, self).get_context_data(**kwargs)
        context['statements'] = context['checkout'].statements.order_by('-at')
        return context

# Checkout - Update

class CheckoutUpdate(SuccessMessageMixin, UpdateView):
    model           = Checkout
    template_name   = 'kfet/checkout_update.html'
    form_class      = CheckoutRestrictForm
    success_message = 'Informations mises à jour pour la caisse : %(name)s'

    # Surcharge de la validation
    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.change_checkout'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)
        # Updating
        return super(CheckoutUpdate, self).form_valid(form)

# -----
# Checkout Statement views
# -----

# Checkout Statement - General

class CheckoutStatementList(ListView):
    model    = CheckoutStatement
    queryset = CheckoutStatement.objects.order_by('-at')
    template_name = 'kfet/checkoutstatement.html'
    context_object_name= 'checkoutstatements'

# Checkout Statement - Create

def getAmountTaken(data):
    return Decimal(data.taken_001 * 0.01 + data.taken_002 * 0.02
        + data.taken_005 * 0.05 + data.taken_01 * 0.1
        + data.taken_02 * 0.2 + data.taken_05 * 0.5
        + data.taken_1 * 1 + data.taken_2 * 2
        + data.taken_5 * 5 + data.taken_10 * 10
        + data.taken_20 * 20 + data.taken_50 * 50
        + data.taken_100 * 100 + data.taken_200 * 200
        + data.taken_500 * 500 + float(data.taken_cheque))

def getAmountBalance(data):
    return Decimal(data['balance_001'] * 0.01 + data['balance_002'] * 0.02
        + data['balance_005'] * 0.05 + data['balance_01'] * 0.1
        + data['balance_02'] * 0.2 + data['balance_05'] * 0.5
        + data['balance_1'] * 1 + data['balance_2'] * 2
        + data['balance_5'] * 5 + data['balance_10'] * 10
        + data['balance_20'] * 20 + data['balance_50'] * 50
        + data['balance_100'] * 100 + data['balance_200'] * 200
        + data['balance_500'] * 500)

class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
    model           = CheckoutStatement
    template_name   = 'kfet/checkoutstatement_create.html'
    form_class      = CheckoutStatementCreateForm
    success_message = 'Nouveau relevé : %(checkout)s - %(at)s'

    def get_success_url(self):
        return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})

    def get_success_message(self, cleaned_data):
        return self.success_message % dict(
            cleaned_data,
            checkout = self.object.checkout.name,
            at = self.object.at)

    def get_context_data(self, **kwargs):
        context = super(CheckoutStatementCreate, self).get_context_data(**kwargs)
        checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
        context['checkout'] = checkout
        return context

    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.add_checkoutstatement'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)
        # Creating
        form.instance.amount_taken = getAmountTaken(form.instance)
        if not form.instance.not_count:
            form.instance.balance_new = getAmountBalance(form.cleaned_data)
        form.instance.checkout_id = self.kwargs['pk_checkout']
        form.instance.by = self.request.user.profile.account_kfet
        return super(CheckoutStatementCreate, self).form_valid(form)

class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
    model = CheckoutStatement
    template_name = 'kfet/checkoutstatement_update.html'
    form_class = CheckoutStatementUpdateForm
    success_message = 'Relevé modifié'

    def get_success_url(self):
        return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})

    def get_context_data(self, **kwargs):
        context = super(CheckoutStatementUpdate, self).get_context_data(**kwargs)
        checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
        context['checkout'] = checkout
        return context

    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.change_checkoutstatement'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)
        # Updating
        form.instance.amount_taken = getAmountTaken(form.instance)
        return super(CheckoutStatementUpdate, self).form_valid(form)

# -----
# Article views
# -----

# Article - General

class ArticleList(ListView):
    queryset = (Article.objects
            .select_related('category')
            .prefetch_related(Prefetch('inventories',
                queryset = Inventory.objects.order_by('-at'),
                to_attr = 'inventory'))
            .order_by('category', '-is_sold', 'name'))
    template_name = 'kfet/article.html'
    context_object_name = 'articles'

# Article - Create

class ArticleCreate(SuccessMessageMixin, CreateView):
    model           = Article
    template_name   = 'kfet/article_create.html'
    form_class      = ArticleForm
    success_message = 'Nouvel item : %(category)s - %(name)s'

    # Surcharge de la validation
    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.add_article'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)

        # Save ici pour save le manytomany suppliers
        article = form.save()
        # Save des suppliers déjà existant
        for supplier in form.cleaned_data['suppliers']:
            SupplierArticle.objects.create(
                    article = article, supplier = supplier)

        # Nouveau supplier
        supplier_new = form.cleaned_data['supplier_new'].strip()
        if supplier_new:
            supplier, created = Supplier.objects.get_or_create(
                    name=supplier_new)
            if created:
                SupplierArticle.objects.create(
                        article = article, supplier = supplier)

        # Inventaire avec stock initial
        inventory = Inventory()
        inventory.by = self.request.user.profile.account_kfet
        inventory.save()
        InventoryArticle.objects.create(
                inventory = inventory,
                article   = article,
                stock_old = article.stock,
                stock_new = article.stock,
            )

        # Creating
        return super(ArticleCreate, self).form_valid(form)

# Article - Read

class ArticleRead(DetailView):
    model         = Article
    template_name = 'kfet/article_read.html'
    context_object_name = 'article'

    def get_context_data(self, **kwargs):
        context = super(ArticleRead, self).get_context_data(**kwargs)
        inventoryarts = (InventoryArticle.objects
                .filter(article = self.object)
                .select_related('inventory')
                .order_by('-inventory__at'))
        context['inventoryarts'] = inventoryarts
        supplierarts = (SupplierArticle.objects
                .filter(article = self.object)
                .select_related('supplier')
                .order_by('-at'))
        context['supplierarts'] = supplierarts
        return context

# Article - Update

class ArticleUpdate(SuccessMessageMixin, UpdateView):
    model           = Article
    template_name   = 'kfet/article_update.html'
    form_class      = ArticleRestrictForm
    success_message = "Informations mises à jour pour l'article : %(name)s"

    # Surcharge de la validation
    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.change_article'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)

        # Save ici pour save le manytomany suppliers
        article = form.save()
        # Save des suppliers déjà existant
        for supplier in form.cleaned_data['suppliers']:
            if supplier not in article.suppliers.all():
                SupplierArticle.objects.create(
                        article = article, supplier = supplier)

        # On vire les suppliers désélectionnés
        for supplier in article.suppliers.all():
            if supplier not in form.cleaned_data['suppliers']:
                SupplierArticle.objects.filter(
                        article = article, supplier = supplier).delete()

        # Nouveau supplier
        supplier_new = form.cleaned_data['supplier_new'].strip()
        if supplier_new:
            supplier, created = Supplier.objects.get_or_create(
                    name=supplier_new)
            if created:
                SupplierArticle.objects.create(
                        article = article, supplier = supplier)

        # Updating
        return super(ArticleUpdate, self).form_valid(form)

# -----
# K-Psul
# -----

@teamkfet_required
def kpsul(request):
    data = {}
    data['operationgroup_form'] = KPsulOperationGroupForm()
    data['trigramme_form'] = KPsulAccountForm()
    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)

@teamkfet_required
def kpsul_get_settings(request):
    addcost_for = Settings.ADDCOST_FOR()
    data = {
        'subvention_cof': Settings.SUBVENTION_COF(),
        'addcost_for'   : addcost_for and addcost_for.trigramme or '',
        'addcost_amount': Settings.ADDCOST_AMOUNT(),
    }
    return JsonResponse(data)

@teamkfet_required
def account_read_json(request):
    trigramme = request.POST.get('trigramme', '')
    account   = get_object_or_404(Account, trigramme=trigramme)
    data = { 'id': account.pk, 'name': account.name, 'email': account.email,
        'is_cof': account.is_cof, 'promo': account.promo,
        'balance': account.balance, 'is_frozen': account.is_frozen,
        'departement': account.departement, 'nickname': account.nickname,
        'trigramme': account.trigramme }
    return JsonResponse(data)

@teamkfet_required
def kpsul_checkout_data(request):
    pk = request.POST.get('pk', 0)
    if not pk:
        pk = 0
    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
def kpsul_update_addcost(request):
    addcost_form = AddcostForm(request.POST)

    if not addcost_form.is_valid():
        data = { 'errors': { 'addcost': list(addcost_form.errors) } }
        return JsonResponse(data, status=400)
    required_perms = ['kfet.manage_addcosts']
    if not request.user.has_perms(required_perms):
        data = {
            'errors': {
                'missing_perms': get_missing_perms(required_perms, request.user)
            }
        }
        return JsonResponse(data, status=403)

    trigramme = addcost_form.cleaned_data['trigramme']
    account = trigramme and Account.objects.get(trigramme=trigramme) or None
    Settings.objects.filter(name='ADDCOST_FOR').update(value_account=account)
    Settings.objects.filter(name='ADDCOST_AMOUNT').update(value_decimal=addcost_form.cleaned_data['amount'])
    cache.delete('ADDCOST_FOR')
    cache.delete('ADDCOST_AMOUNT')
    data = {
        'addcost': {
            'for': trigramme and account.trigramme or None,
            'amount': addcost_form.cleaned_data['amount'],
        }
    }
    consumers.KPsul.group_send('kfet.kpsul', 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

@teamkfet_required
def kpsul_perform_operations(request):
    # Initializing response data
    data = { 'operationgroup': 0, 'operations': [],
             'warnings': {}, 'errors': {} }

    # Checking operationgroup
    operationgroup_form = KPsulOperationGroupForm(request.POST)
    if not operationgroup_form.is_valid():
        data['errors']['operation_group'] = list(operationgroup_form.errors)

    # Checking operation_formset
    operation_formset   = KPsulOperationFormSet(request.POST)
    if not operation_formset.is_valid():
        data['errors']['operations'] = list(operation_formset.errors)

    # Returning BAD REQUEST if errors
    if data['errors']:
        return JsonResponse(data, status=400)

    # Pre-saving (no commit)
    operationgroup = operationgroup_form.save(commit = False)
    operations     = operation_formset.save(commit = False)

    # Retrieving COF grant
    cof_grant = Settings.SUBVENTION_COF()
    # Retrieving addcosts data
    addcost_amount = Settings.ADDCOST_AMOUNT()
    addcost_for    = Settings.ADDCOST_FOR()

    # Initializing vars
    required_perms    = set() # Required perms to perform all operations
    cof_grant_divisor = 1 + cof_grant / 100
    to_addcost_for_balance = 0 # For balance of addcost_for
    to_checkout_balance = 0 # For balance of selected checkout
    to_articles_stocks  = defaultdict(lambda:0) # For stocks articles
    is_addcost = (addcost_for and addcost_amount
                    and addcost_for != operationgroup.on_acc)
    need_comment = operationgroup.on_acc.need_comment

    # Filling data of each operations + operationgroup + calculating other stuffs
    for operation in operations:
        if operation.type == Operation.PURCHASE:
            operation.amount = - operation.article.price * operation.article_nb
            if is_addcost:
                operation.addcost_for    = addcost_for
                operation.addcost_amount = addcost_amount * operation.article_nb
                operation.amount        -= operation.addcost_amount
                to_addcost_for_balance  += operation.addcost_amount
            if operationgroup.on_acc.is_cash:
                operation.is_checkout = True
                to_checkout_balance += -operation.amount
            else:
                operation.is_checkout = False
            if operationgroup.on_acc.is_cof:
                if is_addcost:
                    operation.addcost_amount = operation.addcost_amount / cof_grant_divisor
                operation.amount = operation.amount / cof_grant_divisor
            to_articles_stocks[operation.article] -= operation.article_nb
        else:
            if operationgroup.on_acc.is_cash:
                data['errors']['account'] = 'Charge et retrait impossible sur LIQ'
            to_checkout_balance += operation.amount
        operationgroup.amount += operation.amount
        if operation.type == Operation.DEPOSIT:
            required_perms.add('kfet.perform_deposit')
        if (not operation.is_checkout
                and operation.type in [Operation.DEPOSIT, Operation.WITHDRAW]):
            required_perms.add('kfet.edit_balance_account')
            need_comment = True
    if operationgroup.on_acc.is_cof:
        to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor

    (perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
            amount = operationgroup.amount)
    required_perms |= perms

    if need_comment:
        operationgroup.comment = operationgroup.comment.strip()
        if not operationgroup.comment:
            data['errors']['need_comment'] = True
            return JsonResponse(data, status=400)

    if stop 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:
            data['errors']['negative'] = [operationgroup.on_acc.trigramme]
        return JsonResponse(data, status=403)

    # If 1 perm is required, filling who perform the operations
    if required_perms:
        operationgroup.valid_by = request.user.profile.account_kfet
    # Filling cof status for statistics
    operationgroup.is_cof = operationgroup.on_acc.is_cof

    # Starting transaction to ensure data consistency
    with transaction.atomic():
        # If not cash account,
        # saving account's balance and adding to Negative if not in
        if not operationgroup.on_acc.is_cash:
            Account.objects.filter(pk=operationgroup.on_acc.pk).update(
                balance = F('balance') + operationgroup.amount)
            operationgroup.on_acc.refresh_from_db()
            if operationgroup.on_acc.balance < 0:
                if hasattr(operationgroup.on_acc, 'negative'):
                    if not operationgroup.on_acc.negative.start:
                        operationgroup.on_acc.negative.start = timezone.now()
                        operationgroup.on_acc.negative.save()
                else:
                    negative = AccountNegative(
                        account = operationgroup.on_acc, start = timezone.now())
                    negative.save()
            elif (hasattr(operationgroup.on_acc, 'negative')
                    and not operationgroup.on_acc.negative.balance_offset):
                operationgroup.on_acc.negative.delete()

        # Updating checkout's balance
        if to_checkout_balance:
            Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
                balance = F('balance') + to_checkout_balance)

        # Saving addcost_for with new balance if there is one
        if is_addcost and to_addcost_for_balance:
            Account.objects.filter(pk=addcost_for.pk).update(
                balance = F('balance') + to_addcost_for_balance)

        # Saving operation group
        operationgroup.save()
        data['operationgroup'] = operationgroup.pk

        # Filling operationgroup id for each operations and saving
        for operation in operations:
            operation.group = operationgroup
            operation.save()
            data['operations'].append(operation.pk)

        # Updating articles stock
        for article in to_articles_stocks:
            Article.objects.filter(pk=article.pk).update(
                stock = F('stock') + to_articles_stocks[article])

    # Websocket data
    websocket_data = {}
    websocket_data['opegroups'] = [{
        'add': True,
        'id': operationgroup.pk,
        'amount': operationgroup.amount,
        'checkout__name': operationgroup.checkout.name,
        'at': operationgroup.at,
        'is_cof': operationgroup.is_cof,
        'comment': operationgroup.comment,
        'valid_by__trigramme': ( operationgroup.valid_by and
            operationgroup.valid_by.trigramme or None),
        'on_acc__trigramme': operationgroup.on_acc.trigramme,
        'opes': [],
    }]
    for operation in operations:
        ope_data = {
            'id': operation.pk, 'type': operation.type, 'amount': operation.amount,
            'addcost_amount': operation.addcost_amount,
            'addcost_for__trigramme': is_addcost and addcost_for.trigramme or None,
            'is_checkout': operation.is_checkout,
            'article__name': operation.article and operation.article.name or None,
            'article_nb': operation.article_nb,
            'group_id': operationgroup.pk,
            'canceled_by__trigramme': None, 'canceled_at': None,
        }
        websocket_data['opegroups'][0]['opes'].append(ope_data)
    # Need refresh from db cause we used update on queryset
    operationgroup.checkout.refresh_from_db()
    websocket_data['checkouts'] = [{
        'id': operationgroup.checkout.pk,
        'balance': operationgroup.checkout.balance,
    }]
    websocket_data['articles'] = []
    # Need refresh from db cause we used update on querysets
    articles_pk = [ article.pk for article in to_articles_stocks]
    articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
    for article in articles:
        websocket_data['articles'].append({
            'id': article['id'],
            'stock': article['stock']
        })
    consumers.KPsul.group_send('kfet.kpsul', websocket_data)
    return JsonResponse(data)

@teamkfet_required
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('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
        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
            # Les balances de caisses dont il y a eu un relevé depuis la date
            # de la commande ne doivent pas être modifiées
            # TODO : Prendre en compte le dernier relevé où la caisse a été
            #        comptée et donc modifier les balance_old (et amount_error)
            #        des relevés suivants.
            #        Note : Dans le cas où un CheckoutStatement est mis à jour
            #        par `.save()`, amount_error est recalculé automatiquement,
            #        ce qui n'est pas le cas en faisant un update sur queryset
            # TODO ? : Maj les balance_old de relevés pour modifier l'erreur
            last_statement = (CheckoutStatement.objects
                    .filter(checkout=ope.group.checkout)
                    .order_by('at')
                    .last())
            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
                else:
                    to_checkouts_balances[ope.group.checkout] -= ope.amount

            # Pour les stocks d'articles
            # Les stocks d'articles dont il y a eu un inventaire depuis la date
            # de la commande ne doivent pas être modifiés
            # TODO : Prendre en compte le dernier inventaire où le stock a bien
            #        été compté (pas dans le cas d'une livraison).
            #        Note : si InventoryArticle est maj par .save(), stock_error
            #        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())
                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
        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)

    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():
        (Operation.objects.filter(pk__in=opes)
            .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])
        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])

    # Websocket data
    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 = (OperationGroup.objects
        .values('id','amount','is_cof').filter(pk__in=opegroups_pk))
    for opegroup in opegroups:
        websocket_data['opegroups'].append({
            'cancellation': True,
            'id': opegroup['id'],
            'amount': opegroup['amount'],
            'is_cof': opegroup['is_cof'],
        })
    canceled_by__trigramme = canceled_by and canceled_by.trigramme or None
    for ope in opes:
        websocket_data['opes'].append({
            'cancellation': True,
            'id': ope,
            'canceled_by__trigramme': canceled_by__trigramme,
            '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 = (Checkout.objects
        .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 = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
    for article in articles:
        websocket_data['articles'].append({
            'id': article['id'],
            '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
    return JsonResponse(data)

@login_required
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)

    # Construction de la requête (sur les opérations) pour le prefetch
    queryset_prefetch = Operation.objects.select_related(
            'canceled_by__trigramme', 'addcost_for__trigramme',
            'article__name')

    # Construction de la requête principale
    opegroups = (OperationGroup.objects
        .prefetch_related(Prefetch('opes', queryset = queryset_prefetch))
        .select_related('on_acc__trigramme', 'valid_by__trigramme')
        .order_by('at')
    )
    # Application des filtres
    if from_date:
        opegroups = opegroups.filter(at__gte=from_date)
    if to_date:
        opegroups = opegroups.filter(at__lt=to_date)
    if checkouts:
        opegroups = opegroups.filter(checkout_id__in=checkouts)
    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 = {
            'id'         : opegroup.id,
            'amount'     : opegroup.amount,
            'at'         : opegroup.at,
            'checkout_id': opegroup.checkout_id,
            'is_cof'     : opegroup.is_cof,
            'comment'    : opegroup.comment,
            'opes'       : [],
            'on_acc__trigramme':
                    opegroup.on_acc and opegroup.on_acc.trigramme or None,
        }
        if request.user.has_perm('kfet.is_team'):
            opegroup_dict['valid_by__trigramme'] = (
                opegroup.valid_by and opegroup.valid_by.trigramme or None)
        for ope in opegroup.opes.all():
            ope_dict = {
                'id'            : ope.id,
                'type'          : ope.type,
                'amount'        : ope.amount,
                'article_nb'    : ope.article_nb,
                'is_checkout'   : ope.is_checkout,
                'addcost_amount': ope.addcost_amount,
                'canceled_at'   : ope.canceled_at,
                'article__name':
                    ope.article and ope.article.name or None,
                'addcost_for__trigramme':
                    ope.addcost_for and ope.addcost_for.trigramme or None,
            }
            if request.user.has_perm('kfet.is_team'):
                ope_dict['canceled_by__trigramme'] = (
                    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 })

@teamkfet_required
def kpsul_articles_data(request):
    articles = (
        Article.objects
        .values('id', 'name', 'price', 'stock', 'category_id', 'category__name')
        .filter(is_sold=True))
    return JsonResponse({ 'articles': list(articles) })

@teamkfet_required
def history(request):
    data = {
        'filter_form': FilterHistoryForm(),
        'settings': {
            'subvention_cof': Settings.SUBVENTION_COF(),
        }
    }
    return render(request, 'kfet/history.html', data)

# -----
# Settings views
# -----

class SettingsList(ListView):
    model = Settings
    context_object_name = 'settings'
    template_name = 'kfet/settings.html'

    def get_context_data(self, **kwargs):
        Settings.create_missing()
        return super(SettingsList, self).get_context_data(**kwargs)

class SettingsUpdate(SuccessMessageMixin, UpdateView):
    model = Settings
    form_class = SettingsForm
    template_name = 'kfet/settings_update.html'
    success_message = 'Paramètre %(name)s mis à jour'
    success_url = reverse_lazy('kfet.settings')

    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.change_settings'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)
        # Creating
        Settings.empty_cache()
        return super(SettingsUpdate, self).form_valid(form)

# -----
# Transfer views
# -----

@teamkfet_required
def transfers(request):
    transfergroups = (TransferGroup.objects
            .prefetch_related('transfers')
            .order_by('-at'))
    return render(request, 'kfet/transfers.html', {
        'transfergroups': transfergroups,
    })

@teamkfet_required
def transfers_create(request):
    transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
    return render(request, 'kfet/transfers_create.html',
            { 'transfer_formset': transfer_formset })

@teamkfet_required
def perform_transfers(request):
    data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 }

    # Checking transfer_formset
    transfer_formset = TransferFormSet(request.POST)
    if not transfer_formset.is_valid():
        return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400)

    transfers = transfer_formset.save(commit = False)

    # Initializing vars
    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:
        to_accounts_balances[transfer.from_acc] -= transfer.amount
        to_accounts_balances[transfer.to_acc] += transfer.amount

    stop_all = False

    negative_accounts = []
    # Checking if ok on all accounts
    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)

    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)

    # Creating transfer group
    transfergroup = TransferGroup()
    if required_perms:
        transfergroup.valid_by = request.user.profile.account_kfet

    comment = request.POST.get('comment', '')
    transfergroup.comment = comment.strip()

    with transaction.atomic():
        # Updating balances accounts
        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()

        # Saving transfer group
        transfergroup.save()
        data['transfergroup'] = transfergroup.pk

        # Saving all transfers with group
        for transfer in transfers:
            transfer.group = transfergroup
            transfer.save()
            data['transfers'].append(transfer.pk)

    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')
            .annotate(nb_articles=Count('articles'))
            .order_by('-at'))
    template_name = 'kfet/inventory.html'
    context_object_name = 'inventories'

@teamkfet_required
def inventory_create(request):

    articles = (Article.objects
            .select_related('category')
            .order_by('category__name', 'name')
        )
    initial = []
    for article in articles:
        initial.append({
            'article'  : article.pk,
            'stock_old': article.stock,
            'name'     : article.name,
            'category' : article.category_id,
            'category__name': article.category.name
        })

    cls_formset = formset_factory(
            form  = InventoryArticleForm,
            extra = 0,
        )

    if request.POST:
        formset = cls_formset(request.POST, initial=initial)

        if not request.user.has_perm('kfet.add_inventory'):
            messages.error(request, 'Permission refusée')
        elif formset.is_valid():
            with transaction.atomic():

                articles = Article.objects.select_for_update()
                inventory = Inventory()
                inventory.by = request.user.profile.account_kfet
                saved = False
                for form in formset:
                    if form.cleaned_data['stock_new'] is not None:
                        if not saved:
                            inventory.save()
                            saved = True

                        article = articles.get(pk=form.cleaned_data['article'].pk)
                        stock_old = article.stock
                        stock_new = form.cleaned_data['stock_new']
                        InventoryArticle.objects.create(
                                inventory   = inventory,
                                article     = article,
                                stock_old   = stock_old,
                                stock_new   = stock_new)
                        article.stock = stock_new
                        article.save()
                if saved:
                    messages.success(request, 'Inventaire créé')
                    return redirect('kfet.inventory')
                messages.warning(request, 'Bah alors ? On a rien compté ?')
        else:
            messages.error(request, 'Pas marché')
    else:
        formset = cls_formset(initial = initial)

    return render(request, 'kfet/inventory_create.html', {
        'formset': formset,
    })

class InventoryRead(DetailView):
    model = Inventory
    template_name = 'kfet/inventory_read.html'
    context_object_name = 'inventory'

    def get_context_data(self, **kwargs):
        context = super(InventoryRead, self).get_context_data(**kwargs)
        inventoryarticles = (InventoryArticle.objects
                .select_related('article', 'article__category')
                .filter(inventory = self.object)
                .order_by('article__category__name', 'article__name'))
        context['inventoryarts'] = inventoryarticles
        return context

# -----
# Order views
# -----

class OrderList(ListView):
    queryset = Order.objects.select_related('supplier', 'inventory')
    template_name = 'kfet/order.html'
    context_object_name = 'orders'

    def get_context_data(self, **kwargs):
        context = super(OrderList, self).get_context_data(**kwargs)
        context['suppliers'] = Supplier.objects.order_by('name')
        return context

@teamkfet_required
def order_create(request, pk):
    supplier = get_object_or_404(Supplier, pk=pk)

    articles = (Article.objects
            .filter(suppliers=supplier.pk)
            .select_related('category')
            .order_by('category__name', 'name'))

    initial = []
    today = timezone.now().date()
    sales_q = (Operation.objects
        .select_related('group')
        .filter(article__in=articles, canceled_at=None)
        .values('article'))
    sales_s1 = (sales_q
        .filter(
            group__at__gte = today-timedelta(weeks=5),
            group__at__lt  = today-timedelta(weeks=4))
        .annotate(nb=Sum('article_nb'))
    )
    sales_s1 = { d['article']:d['nb'] for d in sales_s1 }
    sales_s2 = (sales_q
        .filter(
            group__at__gte = today-timedelta(weeks=4),
            group__at__lt  = today-timedelta(weeks=3))
        .annotate(nb=Sum('article_nb'))
    )
    sales_s2 = { d['article']:d['nb'] for d in sales_s2 }
    sales_s3 = (sales_q
        .filter(
            group__at__gte = today-timedelta(weeks=3),
            group__at__lt  = today-timedelta(weeks=2))
        .annotate(nb=Sum('article_nb'))
    )
    sales_s3 = { d['article']:d['nb'] for d in sales_s3 }
    sales_s4 = (sales_q
        .filter(
            group__at__gte = today-timedelta(weeks=2),
            group__at__lt  = today-timedelta(weeks=1))
        .annotate(nb=Sum('article_nb'))
    )
    sales_s4 = { d['article']:d['nb'] for d in sales_s4 }
    sales_s5 = (sales_q
        .filter(group__at__gte = today-timedelta(weeks=1))
        .annotate(nb=Sum('article_nb'))
    )
    sales_s5 = { d['article']:d['nb'] for d in sales_s5 }

    for article in articles:
        v_s1 = sales_s1.get(article.pk, 0)
        v_s2 = sales_s2.get(article.pk, 0)
        v_s3 = sales_s3.get(article.pk, 0)
        v_s4 = sales_s4.get(article.pk, 0)
        v_s5 = sales_s5.get(article.pk, 0)
        v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
        v_3max = heapq.nlargest(3, v_all)
        v_moy = statistics.mean(v_3max)
        v_et = statistics.pstdev(v_3max, v_moy)
        v_prev = v_moy + v_et
        c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
        if article.box_capacity:
            c_rec_temp = c_rec_tot / article.box_capacity
            if c_rec_temp >= 10:
                c_rec = round(c_rec_temp)
            elif c_rec_temp > 5:
                c_rec = 10
            elif c_rec_temp > 2:
                c_rec = 5
            else:
                c_rec = round(c_rec_temp)
        initial.append({
            'article': article.pk,
            'name': article.name,
            'category': article.category_id,
            'category__name': article.category.name,
            'stock': article.stock,
            'box_capacity': article.box_capacity,
            'v_s1': v_s1,
            'v_s2': v_s2,
            'v_s3': v_s3,
            'v_s4': v_s4,
            'v_s5': v_s5,
            'v_moy': round(v_moy),
            'v_et': round(v_et),
            'v_prev': round(v_prev),
            'c_rec': article.box_capacity and c_rec or round(c_rec_tot),
        })

    cls_formset = formset_factory(
            form  = OrderArticleForm,
            extra = 0)

    if request.POST:
        formset = cls_formset(request.POST, initial=initial)

        if not request.user.has_perm('kfet.add_order'):
            messages.error(request, 'Permission refusée')
        elif formset.is_valid():
            order = Order()
            order.supplier = supplier
            saved = False
            for form in formset:
                if form.cleaned_data['quantity_ordered'] is not None:
                    if not saved:
                        order.save()
                        saved = True

                    article = articles.get(pk=form.cleaned_data['article'].pk)
                    q_ordered = form.cleaned_data['quantity_ordered']
                    if article.box_capacity:
                        q_ordered *= article.box_capacity
                    OrderArticle.objects.create(
                            order   = order,
                            article = article,
                            quantity_ordered = q_ordered)
            if saved:
                messages.success(request, 'Commande créée')
                return redirect('kfet.order.read', order.pk)
            messages.warning(request, 'Rien commandé => Pas de commande')
        else:
            messages.error(request, 'Corrigez les erreurs')
    else:
        formset = cls_formset(initial=initial)

    return render(request, 'kfet/order_create.html', {
        'supplier': supplier,
        'formset' : formset,
    })

class OrderRead(DetailView):
    model = Order
    template_name = 'kfet/order_read.html'
    context_object_name = 'order'

    def get_context_data(self, **kwargs):
        context = super(OrderRead, self).get_context_data(**kwargs)
        orderarticles = (OrderArticle.objects
                .select_related('article', 'article__category')
                .filter(order=self.object)
                .order_by('article__category__name', 'article__name')
            )
        context['orderarts'] = orderarticles
        mail = ("Bonjour,\n\nNous voudrions pour le ##DATE## à la K-Fêt de "
                "l'ENS Ulm :")
        category = 0
        for orderarticle in orderarticles:
            if category != orderarticle.article.category:
                category = orderarticle.article.category
                mail += '\n'
            nb = orderarticle.quantity_ordered
            box = ''
            if orderarticle.article.box_capacity:
                nb /= orderarticle.article.box_capacity
                if nb >= 2:
                    box = ' %ss de' % orderarticle.article.box_type
                else:
                    box = ' %s de' % orderarticle.article.box_type
            name = orderarticle.article.name.capitalize()
            mail += "\n- %s%s %s" % (round(nb), box, name)

        mail += ("\n\nMerci d'appeler le numéro suivant lorsque les livreurs "
                 "sont là : ##TELEPHONE##\nCordialement,\n##PRENOM## ##NOM## "
                 ", pour la K-Fêt de l'ENS Ulm")

        context['mail'] = mail
        return context

@teamkfet_required
def order_to_inventory(request, pk):
    order = get_object_or_404(Order, pk=pk)

    if hasattr(order, 'inventory'):
        raise Http404

    articles = (Article.objects
            .filter(orders=order.pk)
            .select_related('category')
            .prefetch_related(Prefetch('orderarticle_set',
                queryset = OrderArticle.objects.filter(order=order),
                to_attr = 'order'))
            .prefetch_related(Prefetch('supplierarticle_set',
                queryset = (SupplierArticle.objects
                            .filter(supplier=order.supplier)
                            .order_by('-at')),
                to_attr = 'supplier'))
            .order_by('category__name', 'name'))

    initial = []
    for article in articles:
        initial.append({
            'article': article.pk,
            'name': article.name,
            'category': article.category_id,
            'category__name': article.category.name,
            'quantity_ordered': article.order[0].quantity_ordered,
            'quantity_received': article.order[0].quantity_ordered,
            'price_HT': article.supplier[0].price_HT,
            'TVA': article.supplier[0].TVA,
            'rights': article.supplier[0].rights,
        })

    cls_formset = formset_factory(OrderArticleToInventoryForm, extra=0)

    if request.method == 'POST':
        formset = cls_formset(request.POST, initial=initial)

        if not request.user.has_perm('kfet.order_to_inventory'):
            messages.error(request, 'Permission refusée')
        elif formset.is_valid():
            with transaction.atomic():
                inventory = Inventory()
                inventory.order = order
                inventory.by = request.user.profile.account_kfet
                inventory.save()
                for form in formset:
                    q_received = form.cleaned_data['quantity_received']
                    article = form.cleaned_data['article']
                    SupplierArticle.objects.create(
                            supplier = order.supplier,
                            article = article,
                            price_HT = form.cleaned_data['price_HT'],
                            TVA = form.cleaned_data['TVA'],
                            rights = form.cleaned_data['rights'])
                    (OrderArticle.objects
                            .filter(order=order, article=article)
                            .update(quantity_received = q_received))
                    InventoryArticle.objects.create(
                            inventory = inventory,
                            article = article,
                            stock_old = article.stock,
                            stock_new = article.stock + q_received)
                    article.stock += q_received
                    if q_received > 0:
                        article.is_sold = True
                    article.save()
            messages.success(request, "C'est tout bon !")
            return redirect('kfet.order')
        else:
            messages.error(request, "Corrigez les erreurs")
    else:
        formset = cls_formset(initial=initial)

    return render(request, 'kfet/order_to_inventory.html', {
        'formset': formset,
    })

class SupplierUpdate(SuccessMessageMixin, UpdateView):
    model         = Supplier
    template_name = 'kfet/supplier_form.html'
    fields        = ['name', 'address', 'email', 'phone', 'comment']
    success_url   = reverse_lazy('kfet.order')
    sucess_message = 'Données fournisseur mis à jour'

    # Surcharge de la validation
    def form_valid(self, form):
        # Checking permission
        if not self.request.user.has_perm('kfet.change_supplier'):
            form.add_error(None, 'Permission refusée')
            return self.form_invalid(form)
        # Updating
        return super(SupplierUpdate, self).form_valid(form)