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

import ast
from urllib.parse import urlencode

from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied
from django.core.cache import cache
from django.views.generic import ListView, DetailView, TemplateView, FormView
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, 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 JsonResponse, Http404
from django.forms import formset_factory
from django.db import transaction
from django.db.models import F, Sum, Prefetch, Count
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from gestioncof.models import CofProfile

from kfet.config import kfet_config
from kfet.decorators import teamkfet_required
from kfet.models import (
    Account, Checkout, Article, AccountNegative,
    CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
    InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
    TransferGroup, Transfer, ArticleCategory)
from kfet.forms import (
    AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
    UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
    AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm,
    GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
    CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
    KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
    KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
    TransferFormSet, InventoryArticleForm, OrderArticleForm,
    OrderArticleToInventoryForm, CategoryForm, KFetConfigForm
    )
from collections import defaultdict
from kfet import consumers
from datetime import timedelta
from decimal import Decimal
import django_cas_ng
import heapq
import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale


class Home(TemplateView):
    template_name = "kfet/home.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        articles = list(
            Article.objects
            .filter(is_sold=True, hidden=False)
            .select_related('category')
            .order_by('category__name')
        )
        pressions, others = [], []
        while len(articles) > 0:
            article = articles.pop()
            if article.category.name == 'Pression':
                pressions.append(article)
            else:
                others.append(article)
        context['pressions'], context['articles'] = pressions, others
        return context

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(TemplateView, self).dispatch(*args, **kwargs)


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

    messages.success(request, "Connecté en utilisateur partagé")

    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)
                    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):
    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

    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,
    })

# 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']
                    account.change_pwd(pwd)
                    account.save()
                    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 Account.objects.get(pk=account.pk).balance >= 0 \
                            and not balance_offset_new:
                        AccountNegative.objects.get(account=account).delete()

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

        # Modification de ses propres informations
        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)
            pwd_form = AccountPwdForm(request.POST)

            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 request.user.has_perm('kfet.is_team') \
                        and pwd_form.is_valid():
                    pwd = pwd_form.cleaned_data['pwd1']
                    account.change_pwd(pwd)
                    account.save()
                    messages.success(
                            request, 'Votre mot de passe a été mis à 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):
    user_pre = Prefetch(
        'user_set',
        queryset=User.objects.select_related('profile__account_kfet'),
    )
    groups = (
        Group.objects
        .filter(name__icontains='K-Fêt')
        .prefetch_related('permissions', user_pre)
    )
    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(SuccessMessageMixin, 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)
        real_balances = (neg.account.real_balance for neg in self.object_list)
        context['negatives_sum'] = sum(real_balances)
        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)

# -----
# Category views
# -----


# Category - General
class CategoryList(ListView):
    queryset = (ArticleCategory.objects
                .prefetch_related('articles')
                .order_by('name'))
    template_name = 'kfet/category.html'
    context_object_name = 'categories'


# Category - Update
class CategoryUpdate(SuccessMessageMixin, UpdateView):
    model = ArticleCategory
    template_name = 'kfet/category_update.html'
    form_class = CategoryForm
    success_url = reverse_lazy('kfet.category')
    success_message = "Informations mises à jour pour la catégorie : %(name)s"

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

        # Updating
        return super(CategoryUpdate, 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__name', '-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()
    data['checkout_form'] = KPsulCheckoutForm()
    data['operation_formset'] = KPsulOperationFormSet(
        queryset=Operation.objects.none(),
    )
    return render(request, 'kfet/kpsul.html', data)


@teamkfet_required
def kpsul_get_settings(request):
    addcost_for = kfet_config.addcost_for
    data = {
        'subvention_cof': kfet_config.subvention_cof,
        'addcost_for': addcost_for and addcost_for.trigramme or '',
        'addcost_amount': kfet_config.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
    amount = addcost_form.cleaned_data['amount']

    kfet_config.set(addcost_for=account,
                    addcost_amount=amount)

    data = {
        'addcost': {
            'for': account and account.trigramme or None,
            'amount': 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 = kfet_config.subvention_cof
    # Retrieving addcosts data
    addcost_amount = kfet_config.addcost_amount
    addcost_for = kfet_config.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 = all((addcost_for, addcost_amount,
                      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.article.category.has_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:
                to_checkout_balance += -operation.amount
            if operationgroup.on_acc.is_cof:
                if is_addcost and operation.article.category.has_addcost:
                    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'] = 'LIQ'
            if operation.type != Operation.EDIT:
                to_checkout_balance += operation.amount
        operationgroup.amount += operation.amount
        if operation.type == Operation.DEPOSIT:
            required_perms.add('kfet.perform_deposit')
        if operation.type == Operation.EDIT:
            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

    if data['errors']:
        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
        on_acc = operationgroup.on_acc
        if not on_acc.is_cash:
            (
                Account.objects
                .filter(pk=on_acc.pk)
                .update(balance=F('balance') + operationgroup.amount)
            )
            on_acc.refresh_from_db()
            on_acc.update_negative()

        # 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': (
                operation.addcost_for and addcost_for.trigramme or None),
            '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 = kfet_config.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.is_checkout:
                    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])
            )
            if not account.is_cash:
                # Should always be true, but we want to be sure
                account.refresh_from_db()
                account.update_negative()
        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,
                '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', 'category__has_addcost')
        .filter(is_sold=True))
    return JsonResponse({ 'articles': list(articles) })


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


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


class SettingsList(TemplateView):
    template_name = 'kfet/settings.html'


class SettingsUpdate(SuccessMessageMixin, FormView):
    form_class = KFetConfigForm
    template_name = 'kfet/settings_update.html'
    success_message = 'Paramètres 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)
        form.save()
        return super().form_valid(form)



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

@teamkfet_required
def transfers(request):
    transfers_pre = Prefetch(
        'transfers',
        queryset=(
            Transfer.objects
            .select_related('from_acc', 'to_acc')
        ),
    )

    transfergroups = (
        TransferGroup.objects
        .select_related('valid_by')
        .prefetch_related(transfers_pre)
        .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 = kfet_config.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)

    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,
            'box_capacity': article.box_capacity or 0,
        })

    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)
        .distinct()
        .select_related('category')
        .order_by('category__name', 'name')
    )

    # Force hit to cache
    articles = list(articles)

    sales_q = (
        Operation.objects
        .select_related('group')
        .filter(article__in=articles, canceled_at=None)
        .values('article')
        .annotate(nb=Sum('article_nb'))
    )
    scale = WeekScale(last=True, n_steps=5, std_chunk=False)
    chunks = scale.chunkify_qs(sales_q, field='group__at')

    sales = [
        {d['article']: d['nb'] for d in chunk}
        for chunk in chunks
    ]

    initial = []

    for article in articles:
        # Get sales for each 5 last weeks
        v_all = [chunk.get(article.pk, 0) for chunk in sales]
        # Take the 3 greatest (eg to avoid 2 weeks of vacations)
        v_3max = heapq.nlargest(3, v_all)
        # Get average and standard deviation
        v_moy = statistics.mean(v_3max)
        v_et = statistics.pstdev(v_3max, v_moy)
        # Expected sales for next week
        v_prev = v_moy + v_et
        # We want to have 1.5 * the expected sales in stock
        # (because sometimes some articles are not delivered)
        c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
        # If ordered quantity is close enough to a level which can led to free
        # boxes, we increase it to this level.
        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_all': v_all,
            '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 = form.cleaned_data['article']
                    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)

    scale.label_fmt = "S -{rev_i}"

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


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

    supplier_prefetch = Prefetch(
        'article__supplierarticle_set',
        queryset=(
            SupplierArticle.objects
            .filter(supplier=order.supplier)
            .order_by('-at')
        ),
        to_attr='supplier',
    )

    order_articles = (
        OrderArticle.objects
        .filter(order=order.pk)
        .select_related('article', 'article__category')
        .prefetch_related(
            supplier_prefetch,
        )
        .order_by('article__category__name', 'article__name')
    )

    initial = []
    for order_article in order_articles:
        article = order_article.article
        initial.append({
            'article': article.pk,
            'name': article.name,
            'category': article.category_id,
            'category__name': article.category.name,
            'quantity_ordered': order_article.quantity_ordered,
            'quantity_received': order_article.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.objects.create(
                    order=order, by=request.user.profile.account_kfet,
                )
                new_supplierarticle = []
                new_inventoryarticle = []
                for form in formset:
                    q_received = form.cleaned_data['quantity_received']
                    article = form.cleaned_data['article']

                    price_HT = form.cleaned_data['price_HT']
                    TVA = form.cleaned_data['TVA']
                    rights = form.cleaned_data['rights']

                    if any((form.initial['price_HT'] != price_HT,
                            form.initial['TVA'] != TVA,
                            form.initial['rights'] != rights)):
                        new_supplierarticle.append(
                            SupplierArticle(
                                supplier=order.supplier,
                                article=article,
                                price_HT=price_HT,
                                TVA=TVA,
                                rights=rights,
                            )
                        )
                    (
                        OrderArticle.objects
                        .filter(order=order, article=article)
                        .update(quantity_received=q_received)
                    )
                    new_inventoryarticle.append(
                        InventoryArticle(
                            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()
                SupplierArticle.objects.bulk_create(new_supplierarticle)
                InventoryArticle.objects.bulk_create(new_inventoryarticle)
            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,
        'order': order,
    })

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)


# ==========
# Statistics
# ==========

# ---------------
# Vues génériques
# ---------------
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
class JSONResponseMixin(object):
    """
    A mixin that can be used to render a JSON response.
    """
    def render_to_json_response(self, context, **response_kwargs):
        """
        Returns a JSON response, transforming 'context' to make the payload.
        """
        return JsonResponse(
            self.get_data(context),
            **response_kwargs
        )

    def get_data(self, context):
        """
        Returns an object that will be serialized as JSON by json.dumps().
        """
        # Note: This is *EXTREMELY* naive; in reality, you'll need
        # to do much more complex handling to ensure that arbitrary
        # objects -- such as Django model instances or querysets
        # -- can be serialized as JSON.
        return context


class JSONDetailView(JSONResponseMixin, BaseDetailView):
    """Returns a DetailView that renders a JSON."""

    def render_to_response(self, context):
        return self.render_to_json_response(context)


class PkUrlMixin(object):

    def get_object(self, *args, **kwargs):
        get_by = self.kwargs.get(self.pk_url_kwarg)
        return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by})


class SingleResumeStat(JSONDetailView):
    """Manifest for a kind of a stat about an object.

    Returns JSON whose payload is an array containing descriptions of a stat:
    url to retrieve data, label, ...

    """
    id_prefix = ''
    nb_default = 0

    stats = []
    url_stat = None

    def get_context_data(self, **kwargs):
        # On n'hérite pas
        object_id = self.object.id
        context = {}
        stats = []
        prefix = '{}_{}'.format(self.id_prefix, object_id)
        for i, stat_def in enumerate(self.stats):
            url_pk = getattr(self.object, self.pk_url_kwarg)
            url_params_d = stat_def.get('url_params', {})
            if len(url_params_d) > 0:
                url_params = '?{}'.format(urlencode(url_params_d))
            else:
                url_params = ''
            stats.append({
                'label': stat_def['label'],
                'btn': 'btn_{}_{}'.format(prefix, i),
                'url': '{url}{params}'.format(
                    url=reverse(self.url_stat, args=[url_pk]),
                    params=url_params,
                ),
            })
        context['id_prefix'] = prefix
        context['content_id'] = "content_%s" % prefix
        context['stats'] = stats
        context['default_stat'] = self.nb_default
        context['object_id'] = object_id
        return context


# -----------------------
# Evolution Balance perso
# -----------------------
ID_PREFIX_ACC_BALANCE = "balance_acc"


class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
    """Manifest for balance stats of an account."""
    model = Account
    context_object_name = 'account'
    pk_url_kwarg = 'trigramme'
    url_stat = 'kfet.account.stat.balance'
    id_prefix = ID_PREFIX_ACC_BALANCE
    stats = [
        {
            'label': 'Tout le temps',
        },
        {
            'label': '1 an',
            'url_params': {'last_days': 365},
        },
        {
            'label': '6 mois',
            'url_params': {'last_days': 183},
        },
        {
            'label': '3 mois',
            'url_params': {'last_days': 90},
        },
        {
            'label': '30 jours',
            'url_params': {'last_days': 30},
        },
    ]
    nb_default = 0

    def get_object(self, *args, **kwargs):
        obj = super().get_object(*args, **kwargs)
        if self.request.user != obj.user:
            raise PermissionDenied
        return obj

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)


class AccountStatBalance(PkUrlMixin, JSONDetailView):
    """Datasets of balance of an account.

    Operations and Transfers are taken into account.

    """
    model = Account
    pk_url_kwarg = 'trigramme'
    context_object_name = 'account'

    def get_changes_list(self, last_days=None, begin_date=None, end_date=None):
        account = self.object

        # prepare filters
        if last_days is not None:
            end_date = timezone.now()
            begin_date = end_date - timezone.timedelta(days=last_days)

        # prepare querysets
        # TODO: retirer les opgroup dont tous les op sont annulées
        opegroups = OperationGroup.objects.filter(on_acc=account)
        transfers = (
            Transfer.objects
            .filter(canceled_at=None)
            .select_related('group')
        )
        recv_transfers = transfers.filter(to_acc=account)
        sent_transfers = transfers.filter(from_acc=account)

        # apply filters
        if begin_date is not None:
            opegroups = opegroups.filter(at__gte=begin_date)
            recv_transfers = recv_transfers.filter(group__at__gte=begin_date)
            sent_transfers = sent_transfers.filter(group__at__gte=begin_date)

        if end_date is not None:
            opegroups = opegroups.filter(at__lte=end_date)
            recv_transfers = recv_transfers.filter(group__at__lte=end_date)
            sent_transfers = sent_transfers.filter(group__at__lte=end_date)

        # On transforme tout ça en une liste de dictionnaires sous la forme
        # {'at': date,
        #  'amount': changement de la balance (négatif si diminue la balance,
        #                                      positif si l'augmente),
        #  'label': text descriptif,
        #  'balance': état de la balance après l'action (0 pour le moment,
        #                                      sera mis à jour lors d'une
        #                                      autre passe)
        # }

        actions = []

        actions.append({
            'at': (begin_date or account.created_at).isoformat(),
            'amount': 0,
            'balance': 0,
        })
        actions.append({
            'at': (end_date or timezone.now()).isoformat(),
            'amount': 0,
            'balance': 0,
        })

        actions += [
            {
                'at': ope_grp.at.isoformat(),
                'amount': ope_grp.amount,
                'balance': 0,
            } for ope_grp in opegroups
        ] + [
            {
                'at': tr.group.at.isoformat(),
                'amount': tr.amount,
                'balance': 0,
            } for tr in recv_transfers
        ] + [
            {
                'at': tr.group.at.isoformat(),
                'amount': -tr.amount,
                'balance': 0,
            } for tr in sent_transfers
        ]
        # Maintenant on trie la liste des actions par ordre du plus récent
        # an plus ancien et on met à jour la balance
        if len(actions) > 1:
            actions = sorted(actions, key=lambda k: k['at'], reverse=True)
            actions[0]['balance'] = account.balance
            for i in range(len(actions)-1):
                actions[i+1]['balance'] = \
                        actions[i]['balance'] - actions[i+1]['amount']
        return actions

    def get_context_data(self, *args, **kwargs):
        context = {}

        last_days = self.request.GET.get('last_days', None)
        if last_days is not None:
            last_days = int(last_days)
        begin_date = self.request.GET.get('begin_date', None)
        end_date = self.request.GET.get('end_date', None)

        changes = self.get_changes_list(
            last_days=last_days,
            begin_date=begin_date, end_date=end_date,
        )

        context['charts'] = [{
            "color": "rgb(255, 99, 132)",
            "label": "Balance",
            "values": changes,
        }]
        context['is_time_chart'] = True
        if len(changes) > 0:
            context['min_date'] = changes[-1]['at']
            context['max_date'] = changes[0]['at']
        # TODO: offset
        return context

    def get_object(self, *args, **kwargs):
        obj = super().get_object(*args, **kwargs)
        if self.request.user != obj.user:
            raise PermissionDenied
        return obj

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(AccountStatBalance, self).dispatch(*args, **kwargs)


# ------------------------
# Consommation personnelle
# ------------------------
ID_PREFIX_ACC_LAST = "last_acc"
ID_PREFIX_ACC_LAST_DAYS = "last_days_acc"
ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc"
ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"


class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
    """Manifest for operations stats of an account."""
    model = Account
    context_object_name = 'account'
    pk_url_kwarg = 'trigramme'
    id_prefix = ID_PREFIX_ACC_LAST
    nb_default = 2
    stats = last_stats_manifest(types=[Operation.PURCHASE])
    url_stat = 'kfet.account.stat.operation'

    def get_object(self, *args, **kwargs):
        obj = super().get_object(*args, **kwargs)
        if self.request.user != obj.user:
            raise PermissionDenied
        return obj

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)


class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
    """Datasets of operations of an account."""
    model = Account
    pk_url_kwarg = 'trigramme'
    context_object_name = 'account'
    id_prefix = ""

    def get_operations(self, scale, types=None):
        # On selectionne les opérations qui correspondent
        # à l'article en question et qui ne sont pas annulées
        # puis on choisi pour chaques intervalle les opérations
        # effectuées dans ces intervalles de temps
        all_operations = (
            Operation.objects
            .filter(group__on_acc=self.object,
                    canceled_at=None)
            .values('article_nb', 'group__at')
            .order_by('group__at')
        )
        if types is not None:
            all_operations = all_operations.filter(type__in=types)
        chunks = scale.get_by_chunks(
            all_operations, field_db='group__at',
            field_callback=(lambda d: d['group__at']),
        )
        return chunks

    def get_context_data(self, *args, **kwargs):
        old_ctx = super().get_context_data(*args, **kwargs)
        context = {'labels': old_ctx['labels']}
        scale = self.scale

        types = self.request.GET.get('types', None)
        if types is not None:
            types = ast.literal_eval(types)

        operations = self.get_operations(types=types, scale=scale)
        # On compte les opérations
        nb_ventes = []
        for chunk in operations:
            ventes = sum(ope['article_nb'] for ope in chunk)
            nb_ventes.append(ventes)

        context['charts'] = [{"color": "rgb(255, 99, 132)",
                              "label": "NB items achetés",
                              "values": nb_ventes}]
        return context

    def get_object(self, *args, **kwargs):
        obj = super().get_object(*args, **kwargs)
        if self.request.user != obj.user:
            raise PermissionDenied
        return obj

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)


# ------------------------
# Article Satistiques Last
# ------------------------
ID_PREFIX_ART_LAST = "last_art"
ID_PREFIX_ART_LAST_DAYS = "last_days_art"
ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"


class ArticleStatSalesList(SingleResumeStat):
    """Manifest for sales stats of an article."""
    model = Article
    context_object_name = 'article'
    id_prefix = ID_PREFIX_ART_LAST
    nb_default = 2
    url_stat = 'kfet.article.stat.sales'
    stats = last_stats_manifest()

    @method_decorator(teamkfet_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)


class ArticleStatSales(ScaleMixin, JSONDetailView):
    """Datasets of sales of an article."""
    model = Article
    context_object_name = 'article'

    def get_context_data(self, *args, **kwargs):
        old_ctx = super().get_context_data(*args, **kwargs)
        context = {'labels': old_ctx['labels']}
        scale = self.scale

        all_purchases = (
            Operation.objects
            .filter(
                type=Operation.PURCHASE,
                article=self.object,
                canceled_at=None,
            )
            .values('group__at', 'article_nb')
            .order_by('group__at')
        )
        liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ')
        liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ')

        chunks_liq = scale.get_by_chunks(
            liq_only, field_db='group__at',
            field_callback=lambda d: d['group__at'],
        )
        chunks_no_liq = scale.get_by_chunks(
            liq_exclude, field_db='group__at',
            field_callback=lambda d: d['group__at'],
        )

        # On compte les opérations
        nb_ventes = []
        nb_accounts = []
        nb_liq = []
        for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq):
            sum_accounts = sum(ope['article_nb'] for ope in chunk_no_liq)
            sum_liq = sum(ope['article_nb'] for ope in chunk_liq)
            nb_ventes.append(sum_accounts + sum_liq)
            nb_accounts.append(sum_accounts)
            nb_liq.append(sum_liq)

        context['charts'] = [{"color": "rgb(255, 99, 132)",
                              "label": "Toutes consommations",
                              "values": nb_ventes},
                             {"color": "rgb(54, 162, 235)",
                              "label": "LIQ",
                              "values": nb_liq},
                             {"color": "rgb(255, 205, 86)",
                              "label": "Comptes K-Fêt",
                              "values": nb_accounts}]
        return context

    @method_decorator(teamkfet_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)