d97a7be819
- graph begin at first operation or later
2414 lines
91 KiB
Python
2414 lines
91 KiB
Python
# -*- 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
|
|
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.decorators import teamkfet_required
|
|
from kfet.models import (
|
|
Account, Checkout, Article, Settings, AccountNegative,
|
|
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
|
|
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
|
|
TransferGroup, Transfer)
|
|
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, SettingsForm,
|
|
TransferFormSet, InventoryArticleForm, OrderArticleForm,
|
|
OrderArticleToInventoryForm
|
|
)
|
|
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
|
|
|
|
|
|
class Home(TemplateView):
|
|
template_name = "kfet/home.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(TemplateView, self).get_context_data(**kwargs)
|
|
articles = Article.objects.all().filter(is_sold=True, hidden=False)
|
|
context['pressions'] = articles.filter(category__name='Pression')
|
|
context['articles'] = (articles.exclude(category__name='Pression')
|
|
.order_by('category'))
|
|
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)
|
|
|
|
if need_cas_logout:
|
|
# Vue de déconnexion de CAS
|
|
return logout_cas
|
|
|
|
return render(request, "kfet/login_genericteam.html")
|
|
|
|
def put_cleaned_data_in_dict(dict, form):
|
|
for field in form.cleaned_data:
|
|
dict[field] = form.cleaned_data[field]
|
|
|
|
# -----
|
|
# Account views
|
|
# -----
|
|
|
|
# Account - General
|
|
|
|
@login_required
|
|
@teamkfet_required
|
|
def account(request):
|
|
accounts = Account.objects.select_related('cofprofile__user').order_by('trigramme')
|
|
return render(request, "kfet/account.html", { 'accounts' : accounts })
|
|
|
|
@login_required
|
|
@teamkfet_required
|
|
def account_is_validandfree_ajax(request):
|
|
if not request.GET.get("trigramme", ''):
|
|
raise Http404
|
|
trigramme = request.GET.get("trigramme")
|
|
data = Account.is_validandfree(trigramme)
|
|
return JsonResponse(data)
|
|
|
|
# Account - Create
|
|
|
|
@login_required
|
|
@teamkfet_required
|
|
def account_create_special(request):
|
|
|
|
# Enregistrement
|
|
if request.method == "POST":
|
|
trigramme_form = AccountTriForm(request.POST, initial={'balance':0})
|
|
balance_form = AccountBalanceForm(request.POST)
|
|
|
|
# Peuplement des forms
|
|
username = request.POST.get('username')
|
|
login_clipper = request.POST.get('login_clipper')
|
|
|
|
forms = get_account_create_forms(
|
|
request, username=username, login_clipper=login_clipper)
|
|
|
|
account_form = forms['account_form']
|
|
cof_form = forms['cof_form']
|
|
user_form = forms['user_form']
|
|
|
|
if all((user_form.is_valid(), cof_form.is_valid(),
|
|
trigramme_form.is_valid(), account_form.is_valid(),
|
|
balance_form.is_valid())):
|
|
# Checking permission
|
|
if not request.user.has_perm('kfet.special_add_account'):
|
|
messages.error(request, 'Permission refusée')
|
|
else:
|
|
data = {}
|
|
# Fill data for Account.save()
|
|
put_cleaned_data_in_dict(data, user_form)
|
|
put_cleaned_data_in_dict(data, cof_form)
|
|
|
|
try:
|
|
account = trigramme_form.save(data = data)
|
|
account_form = AccountNoTriForm(request.POST, instance=account)
|
|
account_form.save()
|
|
balance_form = AccountBalanceForm(request.POST, instance=account)
|
|
balance_form.save()
|
|
amount = balance_form.cleaned_data['balance']
|
|
checkout = Checkout.objects.get(name='Initial')
|
|
is_cof = account.is_cof
|
|
opegroup = OperationGroup.objects.create(
|
|
on_acc=account,
|
|
checkout=checkout,
|
|
amount = amount,
|
|
is_cof = account.is_cof)
|
|
ope = Operation.objects.create(
|
|
group = opegroup,
|
|
type = Operation.INITIAL,
|
|
amount = amount,
|
|
is_checkout = False)
|
|
messages.success(request, 'Compte créé : %s' % account.trigramme)
|
|
return redirect('kfet.account.create')
|
|
except Account.UserHasAccount as e:
|
|
messages.error(request, \
|
|
"Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
|
|
else:
|
|
initial = { 'trigramme': request.GET.get('trigramme', '') }
|
|
trigramme_form = AccountTriForm(initial = initial)
|
|
balance_form = AccountBalanceForm(initial = {'balance': 0})
|
|
account_form = None
|
|
cof_form = None
|
|
user_form = None
|
|
|
|
return render(request, "kfet/account_create_special.html", {
|
|
'trigramme_form': trigramme_form,
|
|
'account_form': account_form,
|
|
'cof_form': cof_form,
|
|
'user_form': user_form,
|
|
'balance_form': balance_form,
|
|
})
|
|
|
|
|
|
# Account - Create
|
|
|
|
@login_required
|
|
@teamkfet_required
|
|
def account_create(request):
|
|
|
|
# Enregistrement
|
|
if request.method == "POST":
|
|
trigramme_form = AccountTriForm(request.POST)
|
|
|
|
# Peuplement des forms
|
|
username = request.POST.get('username')
|
|
login_clipper = request.POST.get('login_clipper')
|
|
|
|
forms = get_account_create_forms(
|
|
request, username=username, login_clipper=login_clipper)
|
|
|
|
account_form = forms['account_form']
|
|
cof_form = forms['cof_form']
|
|
user_form = forms['user_form']
|
|
|
|
if all((user_form.is_valid(), cof_form.is_valid(),
|
|
trigramme_form.is_valid(), account_form.is_valid())):
|
|
# Checking permission
|
|
if not request.user.has_perm('kfet.add_account'):
|
|
messages.error(request, 'Permission refusée')
|
|
else:
|
|
data = {}
|
|
# Fill data for Account.save()
|
|
put_cleaned_data_in_dict(data, user_form)
|
|
put_cleaned_data_in_dict(data, cof_form)
|
|
|
|
try:
|
|
account = trigramme_form.save(data = data)
|
|
account_form = AccountNoTriForm(request.POST, instance=account)
|
|
account_form.save()
|
|
messages.success(request, 'Compte créé : %s' % account.trigramme)
|
|
return redirect('kfet.account.create')
|
|
except Account.UserHasAccount as e:
|
|
messages.error(request, \
|
|
"Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
|
|
else:
|
|
initial = { 'trigramme': request.GET.get('trigramme', '') }
|
|
trigramme_form = AccountTriForm(initial = initial)
|
|
account_form = None
|
|
cof_form = None
|
|
user_form = None
|
|
|
|
return render(request, "kfet/account_create.html", {
|
|
'trigramme_form': trigramme_form,
|
|
'account_form': account_form,
|
|
'cof_form': cof_form,
|
|
'user_form': user_form,
|
|
})
|
|
|
|
def account_form_set_readonly_fields(user_form, cof_form):
|
|
user_form.fields['username'].widget.attrs['readonly'] = True
|
|
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
|
|
cof_form.fields['is_cof'].widget.attrs['disabled'] = True
|
|
|
|
def get_account_create_forms(request=None, username=None, login_clipper=None,
|
|
fullname=None):
|
|
user = None
|
|
clipper = False
|
|
if login_clipper and (login_clipper == username or not username):
|
|
# à partir d'un clipper
|
|
# le user associé à ce clipper ne devrait pas encore exister
|
|
clipper = True
|
|
try:
|
|
# Vérification que clipper ne soit pas déjà dans User
|
|
user = User.objects.get(username=login_clipper)
|
|
# Ici, on nous a menti, le user existe déjà
|
|
username = user.username
|
|
clipper = False
|
|
except User.DoesNotExist:
|
|
# Clipper (sans user déjà existant)
|
|
|
|
# UserForm - Prefill
|
|
user_initial = {
|
|
'username' : login_clipper,
|
|
'email' : "%s@clipper.ens.fr" % login_clipper}
|
|
if fullname:
|
|
# Prefill du nom et prénom
|
|
names = fullname.split()
|
|
# Le premier, c'est le prénom
|
|
user_initial['first_name'] = names[0]
|
|
if len(names) > 1:
|
|
# Si d'autres noms -> tous dans le nom de famille
|
|
user_initial['last_name'] = " ".join(names[1:])
|
|
# CofForm - Prefill
|
|
cof_initial = { 'login_clipper': login_clipper }
|
|
|
|
# Form créations
|
|
if request:
|
|
user_form = UserForm(request.POST, initial=user_initial, from_clipper=True)
|
|
cof_form = CofForm(request.POST, initial=cof_initial)
|
|
else:
|
|
user_form = UserForm(initial=user_initial, from_clipper=True)
|
|
cof_form = CofForm(initial=cof_initial)
|
|
|
|
# Protection (read-only) des champs username et login_clipper
|
|
account_form_set_readonly_fields(user_form, cof_form)
|
|
if username and not clipper:
|
|
try:
|
|
user = User.objects.get(username=username)
|
|
# le user existe déjà
|
|
# récupération du profil cof
|
|
(cof, _) = CofProfile.objects.get_or_create(user=user)
|
|
# UserForm + CofForm - Création à partir des instances existantes
|
|
if request:
|
|
user_form = UserForm(request.POST, instance = user)
|
|
cof_form = CofForm(request.POST, instance = cof)
|
|
else:
|
|
user_form = UserForm(instance=user)
|
|
cof_form = CofForm(instance=cof)
|
|
# Protection (read-only) des champs username, login_clipper et is_cof
|
|
account_form_set_readonly_fields(user_form, cof_form)
|
|
except User.DoesNotExist:
|
|
# le username donnée n'existe pas -> Création depuis rien
|
|
# (éventuellement en cours avec erreurs précédemment)
|
|
pass
|
|
if not user and not clipper:
|
|
# connaît pas du tout, faut tout remplir
|
|
if request:
|
|
user_form = UserForm(request.POST)
|
|
cof_form = CofForm(request.POST)
|
|
else:
|
|
user_form = UserForm()
|
|
cof_form = CofForm()
|
|
# mais on laisse le username en écriture
|
|
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
|
|
cof_form.fields['is_cof'].widget.attrs['disabled'] = True
|
|
|
|
if request:
|
|
account_form = AccountNoTriForm(request.POST)
|
|
else:
|
|
account_form = AccountNoTriForm()
|
|
|
|
return {
|
|
'account_form': account_form,
|
|
'cof_form': cof_form,
|
|
'user_form': user_form,
|
|
}
|
|
|
|
|
|
@login_required
|
|
@teamkfet_required
|
|
def account_create_ajax(request, username=None, login_clipper=None,
|
|
fullname=None):
|
|
forms = get_account_create_forms(
|
|
request=None, username=username, login_clipper=login_clipper,
|
|
fullname=fullname)
|
|
return render(request, "kfet/account_create_form.html", {
|
|
'account_form' : forms['account_form'],
|
|
'cof_form' : forms['cof_form'],
|
|
'user_form' : forms['user_form'],
|
|
})
|
|
|
|
# Account - Read
|
|
|
|
@login_required
|
|
def account_read(request, trigramme):
|
|
try:
|
|
account = Account.objects.select_related('negative').get(trigramme=trigramme)
|
|
except Account.DoesNotExist:
|
|
raise Http404
|
|
|
|
# Checking permissions
|
|
if not request.user.has_perm('kfet.is_team') \
|
|
and request.user != account.user:
|
|
raise PermissionDenied
|
|
|
|
addcosts = (OperationGroup.objects
|
|
.filter(opes__addcost_for=account,opes__canceled_at=None)
|
|
.extra({'date':"date(at)"})
|
|
.values('date')
|
|
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
|
|
.order_by('-date'))
|
|
|
|
return render(request, "kfet/account_read.html", {
|
|
'account' : account,
|
|
'addcosts': addcosts,
|
|
'settings': { 'subvention_cof': Settings.SUBVENTION_COF() },
|
|
})
|
|
|
|
# Account - Update
|
|
|
|
|
|
@login_required
|
|
def account_update(request, trigramme):
|
|
account = get_object_or_404(Account, trigramme=trigramme)
|
|
|
|
# Checking permissions
|
|
if not request.user.has_perm('kfet.is_team') \
|
|
and request.user != account.user:
|
|
raise PermissionDenied
|
|
|
|
if request.user.has_perm('kfet.is_team'):
|
|
user_form = UserRestrictTeamForm(instance=account.user)
|
|
group_form = UserGroupForm(instance=account.user)
|
|
account_form = AccountForm(instance=account)
|
|
cof_form = CofRestrictForm(instance=account.cofprofile)
|
|
pwd_form = AccountPwdForm()
|
|
if account.balance < 0 and not hasattr(account, 'negative'):
|
|
AccountNegative.objects.create(account=account,
|
|
start=timezone.now())
|
|
account.refresh_from_db()
|
|
if hasattr(account, 'negative'):
|
|
negative_form = AccountNegativeForm(instance=account.negative)
|
|
else:
|
|
negative_form = None
|
|
else:
|
|
user_form = UserRestrictForm(instance=account.user)
|
|
account_form = AccountRestrictForm(instance=account)
|
|
cof_form = None
|
|
group_form = None
|
|
negative_form = None
|
|
pwd_form = None
|
|
|
|
if request.method == "POST":
|
|
# Update attempt
|
|
success = False
|
|
missing_perm = True
|
|
|
|
if request.user.has_perm('kfet.is_team'):
|
|
account_form = AccountForm(request.POST, instance=account)
|
|
cof_form = CofRestrictForm(request.POST,
|
|
instance=account.cofprofile)
|
|
user_form = UserRestrictTeamForm(request.POST,
|
|
instance=account.user)
|
|
group_form = UserGroupForm(request.POST, instance=account.user)
|
|
pwd_form = AccountPwdForm(request.POST)
|
|
if hasattr(account, 'negative'):
|
|
negative_form = AccountNegativeForm(request.POST,
|
|
instance=account.negative)
|
|
|
|
if (request.user.has_perm('kfet.change_account')
|
|
and account_form.is_valid() and cof_form.is_valid()
|
|
and user_form.is_valid()):
|
|
missing_perm = False
|
|
data = {}
|
|
# Fill data for Account.save()
|
|
put_cleaned_data_in_dict(data, user_form)
|
|
put_cleaned_data_in_dict(data, cof_form)
|
|
|
|
# Updating
|
|
account_form.save(data=data)
|
|
|
|
# Checking perm to update password
|
|
if (request.user.has_perm('kfet.change_account_password')
|
|
and pwd_form.is_valid()):
|
|
pwd = pwd_form.cleaned_data['pwd1']
|
|
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):
|
|
groups = (Group.objects
|
|
.filter(name__icontains='K-Fêt')
|
|
.prefetch_related('permissions', 'user_set__profile__account_kfet')
|
|
)
|
|
return render(request, 'kfet/account_group.html', { 'groups': groups })
|
|
|
|
class AccountGroupCreate(SuccessMessageMixin, CreateView):
|
|
model = Group
|
|
template_name = 'kfet/account_group_form.html'
|
|
form_class = GroupForm
|
|
success_message = 'Nouveau groupe : %(name)s'
|
|
success_url = reverse_lazy('kfet.account.group')
|
|
|
|
class AccountGroupUpdate(UpdateView):
|
|
queryset = Group.objects.filter(name__icontains='K-Fêt')
|
|
template_name = 'kfet/account_group_form.html'
|
|
form_class = GroupForm
|
|
success_message = 'Groupe modifié : %(name)s'
|
|
success_url = reverse_lazy('kfet.account.group')
|
|
|
|
class AccountNegativeList(ListView):
|
|
queryset = (AccountNegative.objects
|
|
.select_related('account', 'account__cofprofile__user')
|
|
.exclude(account__trigramme='#13'))
|
|
template_name = 'kfet/account_negative.html'
|
|
context_object_name = 'negatives'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(AccountNegativeList, self).get_context_data(**kwargs)
|
|
context['settings'] = {
|
|
'overdraft_amount': Settings.OVERDRAFT_AMOUNT(),
|
|
'overdraft_duration': Settings.OVERDRAFT_DURATION(),
|
|
}
|
|
negs_sum = (AccountNegative.objects
|
|
.exclude(account__trigramme='#13')
|
|
.aggregate(
|
|
bal = Coalesce(Sum('account__balance'),0),
|
|
offset = Coalesce(Sum('balance_offset'),0),
|
|
)
|
|
)
|
|
context['negatives_sum'] = negs_sum['bal'] - negs_sum['offset']
|
|
return context
|
|
|
|
# -----
|
|
# Checkout views
|
|
# -----
|
|
|
|
# Checkout - General
|
|
|
|
class CheckoutList(ListView):
|
|
model = Checkout
|
|
template_name = 'kfet/checkout.html'
|
|
context_object_name = 'checkouts'
|
|
|
|
# Checkout - Create
|
|
|
|
class CheckoutCreate(SuccessMessageMixin, CreateView):
|
|
model = Checkout
|
|
template_name = 'kfet/checkout_create.html'
|
|
form_class = CheckoutForm
|
|
success_message = 'Nouvelle caisse : %(name)s'
|
|
|
|
# Surcharge de la validation
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.add_checkout'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
|
|
# Creating
|
|
form.instance.created_by = self.request.user.profile.account_kfet
|
|
checkout = form.save()
|
|
|
|
# Création d'un relevé avec balance initiale
|
|
CheckoutStatement.objects.create(
|
|
checkout = checkout,
|
|
by = self.request.user.profile.account_kfet,
|
|
balance_old = checkout.balance,
|
|
balance_new = checkout.balance,
|
|
amount_taken = 0)
|
|
|
|
return super(CheckoutCreate, self).form_valid(form)
|
|
|
|
# Checkout - Read
|
|
|
|
class CheckoutRead(DetailView):
|
|
model = Checkout
|
|
template_name = 'kfet/checkout_read.html'
|
|
context_object_name = 'checkout'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(CheckoutRead, self).get_context_data(**kwargs)
|
|
context['statements'] = context['checkout'].statements.order_by('-at')
|
|
return context
|
|
|
|
# Checkout - Update
|
|
|
|
class CheckoutUpdate(SuccessMessageMixin, UpdateView):
|
|
model = Checkout
|
|
template_name = 'kfet/checkout_update.html'
|
|
form_class = CheckoutRestrictForm
|
|
success_message = 'Informations mises à jour pour la caisse : %(name)s'
|
|
|
|
# Surcharge de la validation
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.change_checkout'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
# Updating
|
|
return super(CheckoutUpdate, self).form_valid(form)
|
|
|
|
# -----
|
|
# Checkout Statement views
|
|
# -----
|
|
|
|
# Checkout Statement - General
|
|
|
|
class CheckoutStatementList(ListView):
|
|
model = CheckoutStatement
|
|
queryset = CheckoutStatement.objects.order_by('-at')
|
|
template_name = 'kfet/checkoutstatement.html'
|
|
context_object_name= 'checkoutstatements'
|
|
|
|
# Checkout Statement - Create
|
|
|
|
def getAmountTaken(data):
|
|
return Decimal(data.taken_001 * 0.01 + data.taken_002 * 0.02
|
|
+ data.taken_005 * 0.05 + data.taken_01 * 0.1
|
|
+ data.taken_02 * 0.2 + data.taken_05 * 0.5
|
|
+ data.taken_1 * 1 + data.taken_2 * 2
|
|
+ data.taken_5 * 5 + data.taken_10 * 10
|
|
+ data.taken_20 * 20 + data.taken_50 * 50
|
|
+ data.taken_100 * 100 + data.taken_200 * 200
|
|
+ data.taken_500 * 500 + float(data.taken_cheque))
|
|
|
|
def getAmountBalance(data):
|
|
return Decimal(data['balance_001'] * 0.01 + data['balance_002'] * 0.02
|
|
+ data['balance_005'] * 0.05 + data['balance_01'] * 0.1
|
|
+ data['balance_02'] * 0.2 + data['balance_05'] * 0.5
|
|
+ data['balance_1'] * 1 + data['balance_2'] * 2
|
|
+ data['balance_5'] * 5 + data['balance_10'] * 10
|
|
+ data['balance_20'] * 20 + data['balance_50'] * 50
|
|
+ data['balance_100'] * 100 + data['balance_200'] * 200
|
|
+ data['balance_500'] * 500)
|
|
|
|
class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
|
|
model = CheckoutStatement
|
|
template_name = 'kfet/checkoutstatement_create.html'
|
|
form_class = CheckoutStatementCreateForm
|
|
success_message = 'Nouveau relevé : %(checkout)s - %(at)s'
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})
|
|
|
|
def get_success_message(self, cleaned_data):
|
|
return self.success_message % dict(
|
|
cleaned_data,
|
|
checkout = self.object.checkout.name,
|
|
at = self.object.at)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(CheckoutStatementCreate, self).get_context_data(**kwargs)
|
|
checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
|
|
context['checkout'] = checkout
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.add_checkoutstatement'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
# Creating
|
|
form.instance.amount_taken = getAmountTaken(form.instance)
|
|
if not form.instance.not_count:
|
|
form.instance.balance_new = getAmountBalance(form.cleaned_data)
|
|
form.instance.checkout_id = self.kwargs['pk_checkout']
|
|
form.instance.by = self.request.user.profile.account_kfet
|
|
return super(CheckoutStatementCreate, self).form_valid(form)
|
|
|
|
class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
|
|
model = CheckoutStatement
|
|
template_name = 'kfet/checkoutstatement_update.html'
|
|
form_class = CheckoutStatementUpdateForm
|
|
success_message = 'Relevé modifié'
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(CheckoutStatementUpdate, self).get_context_data(**kwargs)
|
|
checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
|
|
context['checkout'] = checkout
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.change_checkoutstatement'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
# Updating
|
|
form.instance.amount_taken = getAmountTaken(form.instance)
|
|
return super(CheckoutStatementUpdate, self).form_valid(form)
|
|
|
|
# -----
|
|
# Article views
|
|
# -----
|
|
|
|
# Article - General
|
|
|
|
class ArticleList(ListView):
|
|
queryset = (Article.objects
|
|
.select_related('category')
|
|
.prefetch_related(Prefetch('inventories',
|
|
queryset = Inventory.objects.order_by('-at'),
|
|
to_attr = 'inventory'))
|
|
.order_by('category', '-is_sold', 'name'))
|
|
template_name = 'kfet/article.html'
|
|
context_object_name = 'articles'
|
|
|
|
# Article - Create
|
|
|
|
class ArticleCreate(SuccessMessageMixin, CreateView):
|
|
model = Article
|
|
template_name = 'kfet/article_create.html'
|
|
form_class = ArticleForm
|
|
success_message = 'Nouvel item : %(category)s - %(name)s'
|
|
|
|
# Surcharge de la validation
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.add_article'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
|
|
# Save ici pour save le manytomany suppliers
|
|
article = form.save()
|
|
# Save des suppliers déjà existant
|
|
for supplier in form.cleaned_data['suppliers']:
|
|
SupplierArticle.objects.create(
|
|
article = article, supplier = supplier)
|
|
|
|
# Nouveau supplier
|
|
supplier_new = form.cleaned_data['supplier_new'].strip()
|
|
if supplier_new:
|
|
supplier, created = Supplier.objects.get_or_create(
|
|
name=supplier_new)
|
|
if created:
|
|
SupplierArticle.objects.create(
|
|
article = article, supplier = supplier)
|
|
|
|
# Inventaire avec stock initial
|
|
inventory = Inventory()
|
|
inventory.by = self.request.user.profile.account_kfet
|
|
inventory.save()
|
|
InventoryArticle.objects.create(
|
|
inventory = inventory,
|
|
article = article,
|
|
stock_old = article.stock,
|
|
stock_new = article.stock,
|
|
)
|
|
|
|
# Creating
|
|
return super(ArticleCreate, self).form_valid(form)
|
|
|
|
# Article - Read
|
|
|
|
class ArticleRead(DetailView):
|
|
model = Article
|
|
template_name = 'kfet/article_read.html'
|
|
context_object_name = 'article'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ArticleRead, self).get_context_data(**kwargs)
|
|
inventoryarts = (InventoryArticle.objects
|
|
.filter(article = self.object)
|
|
.select_related('inventory')
|
|
.order_by('-inventory__at'))
|
|
context['inventoryarts'] = inventoryarts
|
|
supplierarts = (SupplierArticle.objects
|
|
.filter(article = self.object)
|
|
.select_related('supplier')
|
|
.order_by('-at'))
|
|
context['supplierarts'] = supplierarts
|
|
return context
|
|
|
|
# Article - Update
|
|
|
|
class ArticleUpdate(SuccessMessageMixin, UpdateView):
|
|
model = Article
|
|
template_name = 'kfet/article_update.html'
|
|
form_class = ArticleRestrictForm
|
|
success_message = "Informations mises à jour pour l'article : %(name)s"
|
|
|
|
# Surcharge de la validation
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.change_article'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
|
|
# Save ici pour save le manytomany suppliers
|
|
article = form.save()
|
|
# Save des suppliers déjà existant
|
|
for supplier in form.cleaned_data['suppliers']:
|
|
if supplier not in article.suppliers.all():
|
|
SupplierArticle.objects.create(
|
|
article = article, supplier = supplier)
|
|
|
|
# On vire les suppliers désélectionnés
|
|
for supplier in article.suppliers.all():
|
|
if supplier not in form.cleaned_data['suppliers']:
|
|
SupplierArticle.objects.filter(
|
|
article = article, supplier = supplier).delete()
|
|
|
|
# Nouveau supplier
|
|
supplier_new = form.cleaned_data['supplier_new'].strip()
|
|
if supplier_new:
|
|
supplier, created = Supplier.objects.get_or_create(
|
|
name=supplier_new)
|
|
if created:
|
|
SupplierArticle.objects.create(
|
|
article = article, supplier = supplier)
|
|
|
|
# Updating
|
|
return super(ArticleUpdate, self).form_valid(form)
|
|
|
|
|
|
|
|
# -----
|
|
# K-Psul
|
|
# -----
|
|
|
|
@teamkfet_required
|
|
def kpsul(request):
|
|
data = {}
|
|
data['operationgroup_form'] = KPsulOperationGroupForm()
|
|
data['trigramme_form'] = KPsulAccountForm()
|
|
initial = {}
|
|
try:
|
|
checkout = Checkout.objects.filter(
|
|
is_protected=False, valid_from__lte=timezone.now(),
|
|
valid_to__gte=timezone.now()).get()
|
|
initial['checkout'] = checkout
|
|
except (Checkout.DoesNotExist, Checkout.MultipleObjectsReturned):
|
|
pass
|
|
data['checkout_form'] = KPsulCheckoutForm(initial=initial)
|
|
operation_formset = KPsulOperationFormSet(queryset=Operation.objects.none())
|
|
data['operation_formset'] = operation_formset
|
|
return render(request, 'kfet/kpsul.html', data)
|
|
|
|
@teamkfet_required
|
|
def kpsul_get_settings(request):
|
|
addcost_for = Settings.ADDCOST_FOR()
|
|
data = {
|
|
'subvention_cof': Settings.SUBVENTION_COF(),
|
|
'addcost_for' : addcost_for and addcost_for.trigramme or '',
|
|
'addcost_amount': Settings.ADDCOST_AMOUNT(),
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
@teamkfet_required
|
|
def account_read_json(request):
|
|
trigramme = request.POST.get('trigramme', '')
|
|
account = get_object_or_404(Account, trigramme=trigramme)
|
|
data = { 'id': account.pk, 'name': account.name, 'email': account.email,
|
|
'is_cof': account.is_cof, 'promo': account.promo,
|
|
'balance': account.balance, 'is_frozen': account.is_frozen,
|
|
'departement': account.departement, 'nickname': account.nickname,
|
|
'trigramme': account.trigramme }
|
|
return JsonResponse(data)
|
|
|
|
@teamkfet_required
|
|
def kpsul_checkout_data(request):
|
|
pk = request.POST.get('pk', 0)
|
|
if not pk:
|
|
pk = 0
|
|
data = (Checkout.objects
|
|
.annotate(
|
|
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
|
|
last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'),
|
|
last_statement_by_trigramme=F('statements__by__trigramme'),
|
|
last_statement_balance=F('statements__balance_new'),
|
|
last_statement_at=F('statements__at'))
|
|
.values(
|
|
'id', 'name', 'balance', 'valid_from', 'valid_to',
|
|
'last_statement_balance', 'last_statement_at',
|
|
'last_statement_by_trigramme', 'last_statement_by_last_name',
|
|
'last_statement_by_first_name')
|
|
.select_related(
|
|
'statements'
|
|
'statements__by',
|
|
'statements__by__cofprofile__user')
|
|
.filter(pk=pk)
|
|
.order_by('statements__at')
|
|
.last())
|
|
if data is None:
|
|
raise Http404
|
|
return JsonResponse(data)
|
|
|
|
@teamkfet_required
|
|
def kpsul_update_addcost(request):
|
|
addcost_form = AddcostForm(request.POST)
|
|
|
|
if not addcost_form.is_valid():
|
|
data = { 'errors': { 'addcost': list(addcost_form.errors) } }
|
|
return JsonResponse(data, status=400)
|
|
required_perms = ['kfet.manage_addcosts']
|
|
if not request.user.has_perms(required_perms):
|
|
data = {
|
|
'errors': {
|
|
'missing_perms': get_missing_perms(required_perms, request.user)
|
|
}
|
|
}
|
|
return JsonResponse(data, status=403)
|
|
|
|
trigramme = addcost_form.cleaned_data['trigramme']
|
|
account = trigramme and Account.objects.get(trigramme=trigramme) or None
|
|
Settings.objects.filter(name='ADDCOST_FOR').update(value_account=account)
|
|
Settings.objects.filter(name='ADDCOST_AMOUNT').update(value_decimal=addcost_form.cleaned_data['amount'])
|
|
cache.delete('ADDCOST_FOR')
|
|
cache.delete('ADDCOST_AMOUNT')
|
|
data = {
|
|
'addcost': {
|
|
'for': trigramme and account.trigramme or None,
|
|
'amount': addcost_form.cleaned_data['amount'],
|
|
}
|
|
}
|
|
consumers.KPsul.group_send('kfet.kpsul', data)
|
|
return JsonResponse(data)
|
|
|
|
def get_missing_perms(required_perms, user):
|
|
missing_perms_codenames = [ (perm.split('.'))[1]
|
|
for perm in required_perms if not user.has_perm(perm)]
|
|
missing_perms = list(
|
|
Permission.objects
|
|
.filter(codename__in=missing_perms_codenames)
|
|
.values_list('name', flat=True))
|
|
return missing_perms
|
|
|
|
@teamkfet_required
|
|
def kpsul_perform_operations(request):
|
|
# Initializing response data
|
|
data = { 'operationgroup': 0, 'operations': [],
|
|
'warnings': {}, 'errors': {} }
|
|
|
|
# Checking operationgroup
|
|
operationgroup_form = KPsulOperationGroupForm(request.POST)
|
|
if not operationgroup_form.is_valid():
|
|
data['errors']['operation_group'] = list(operationgroup_form.errors)
|
|
|
|
# Checking operation_formset
|
|
operation_formset = KPsulOperationFormSet(request.POST)
|
|
if not operation_formset.is_valid():
|
|
data['errors']['operations'] = list(operation_formset.errors)
|
|
|
|
# Returning BAD REQUEST if errors
|
|
if data['errors']:
|
|
return JsonResponse(data, status=400)
|
|
|
|
# Pre-saving (no commit)
|
|
operationgroup = operationgroup_form.save(commit = False)
|
|
operations = operation_formset.save(commit = False)
|
|
|
|
# Retrieving COF grant
|
|
cof_grant = Settings.SUBVENTION_COF()
|
|
# Retrieving addcosts data
|
|
addcost_amount = Settings.ADDCOST_AMOUNT()
|
|
addcost_for = Settings.ADDCOST_FOR()
|
|
|
|
# Initializing vars
|
|
required_perms = set() # Required perms to perform all operations
|
|
cof_grant_divisor = 1 + cof_grant / 100
|
|
to_addcost_for_balance = 0 # For balance of addcost_for
|
|
to_checkout_balance = 0 # For balance of selected checkout
|
|
to_articles_stocks = defaultdict(lambda:0) # For stocks articles
|
|
is_addcost = (addcost_for and addcost_amount
|
|
and addcost_for != operationgroup.on_acc)
|
|
need_comment = operationgroup.on_acc.need_comment
|
|
|
|
# Filling data of each operations + operationgroup + calculating other stuffs
|
|
for operation in operations:
|
|
if operation.type == Operation.PURCHASE:
|
|
operation.amount = - operation.article.price * operation.article_nb
|
|
if is_addcost:
|
|
operation.addcost_for = addcost_for
|
|
operation.addcost_amount = addcost_amount * operation.article_nb
|
|
operation.amount -= operation.addcost_amount
|
|
to_addcost_for_balance += operation.addcost_amount
|
|
if operationgroup.on_acc.is_cash:
|
|
operation.is_checkout = True
|
|
to_checkout_balance += -operation.amount
|
|
else:
|
|
operation.is_checkout = False
|
|
if operationgroup.on_acc.is_cof:
|
|
if is_addcost:
|
|
operation.addcost_amount = operation.addcost_amount / cof_grant_divisor
|
|
operation.amount = operation.amount / cof_grant_divisor
|
|
to_articles_stocks[operation.article] -= operation.article_nb
|
|
else:
|
|
if operationgroup.on_acc.is_cash:
|
|
data['errors']['account'] = 'Charge et retrait impossible sur LIQ'
|
|
to_checkout_balance += operation.amount
|
|
operationgroup.amount += operation.amount
|
|
if operation.type == Operation.DEPOSIT:
|
|
required_perms.add('kfet.perform_deposit')
|
|
if (not operation.is_checkout
|
|
and operation.type in [Operation.DEPOSIT, Operation.WITHDRAW]):
|
|
required_perms.add('kfet.edit_balance_account')
|
|
need_comment = True
|
|
if operationgroup.on_acc.is_cof:
|
|
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
|
|
|
|
(perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
|
|
amount = operationgroup.amount)
|
|
required_perms |= perms
|
|
|
|
if need_comment:
|
|
operationgroup.comment = operationgroup.comment.strip()
|
|
if not operationgroup.comment:
|
|
data['errors']['need_comment'] = True
|
|
|
|
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
|
|
if not operationgroup.on_acc.is_cash:
|
|
Account.objects.filter(pk=operationgroup.on_acc.pk).update(
|
|
balance = F('balance') + operationgroup.amount)
|
|
operationgroup.on_acc.refresh_from_db()
|
|
if operationgroup.on_acc.balance < 0:
|
|
if hasattr(operationgroup.on_acc, 'negative'):
|
|
if not operationgroup.on_acc.negative.start:
|
|
operationgroup.on_acc.negative.start = timezone.now()
|
|
operationgroup.on_acc.negative.save()
|
|
else:
|
|
negative = AccountNegative(
|
|
account = operationgroup.on_acc, start = timezone.now())
|
|
negative.save()
|
|
elif (hasattr(operationgroup.on_acc, 'negative')
|
|
and not operationgroup.on_acc.negative.balance_offset):
|
|
operationgroup.on_acc.negative.delete()
|
|
|
|
# Updating checkout's balance
|
|
if to_checkout_balance:
|
|
Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
|
|
balance = F('balance') + to_checkout_balance)
|
|
|
|
# Saving addcost_for with new balance if there is one
|
|
if is_addcost and to_addcost_for_balance:
|
|
Account.objects.filter(pk=addcost_for.pk).update(
|
|
balance = F('balance') + to_addcost_for_balance)
|
|
|
|
# Saving operation group
|
|
operationgroup.save()
|
|
data['operationgroup'] = operationgroup.pk
|
|
|
|
# Filling operationgroup id for each operations and saving
|
|
for operation in operations:
|
|
operation.group = operationgroup
|
|
operation.save()
|
|
data['operations'].append(operation.pk)
|
|
|
|
# Updating articles stock
|
|
for article in to_articles_stocks:
|
|
Article.objects.filter(pk=article.pk).update(
|
|
stock = F('stock') + to_articles_stocks[article])
|
|
|
|
# Websocket data
|
|
websocket_data = {}
|
|
websocket_data['opegroups'] = [{
|
|
'add': True,
|
|
'id': operationgroup.pk,
|
|
'amount': operationgroup.amount,
|
|
'checkout__name': operationgroup.checkout.name,
|
|
'at': operationgroup.at,
|
|
'is_cof': operationgroup.is_cof,
|
|
'comment': operationgroup.comment,
|
|
'valid_by__trigramme': ( operationgroup.valid_by and
|
|
operationgroup.valid_by.trigramme or None),
|
|
'on_acc__trigramme': operationgroup.on_acc.trigramme,
|
|
'opes': [],
|
|
}]
|
|
for operation in operations:
|
|
ope_data = {
|
|
'id': operation.pk, 'type': operation.type, 'amount': operation.amount,
|
|
'addcost_amount': operation.addcost_amount,
|
|
'addcost_for__trigramme': is_addcost and addcost_for.trigramme or None,
|
|
'is_checkout': operation.is_checkout,
|
|
'article__name': operation.article and operation.article.name or None,
|
|
'article_nb': operation.article_nb,
|
|
'group_id': operationgroup.pk,
|
|
'canceled_by__trigramme': None, 'canceled_at': None,
|
|
}
|
|
websocket_data['opegroups'][0]['opes'].append(ope_data)
|
|
# Need refresh from db cause we used update on queryset
|
|
operationgroup.checkout.refresh_from_db()
|
|
websocket_data['checkouts'] = [{
|
|
'id': operationgroup.checkout.pk,
|
|
'balance': operationgroup.checkout.balance,
|
|
}]
|
|
websocket_data['articles'] = []
|
|
# Need refresh from db cause we used update on querysets
|
|
articles_pk = [ article.pk for article in to_articles_stocks]
|
|
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
|
|
for article in articles:
|
|
websocket_data['articles'].append({
|
|
'id': article['id'],
|
|
'stock': article['stock']
|
|
})
|
|
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
|
|
return JsonResponse(data)
|
|
|
|
@teamkfet_required
|
|
def kpsul_cancel_operations(request):
|
|
# Pour la réponse
|
|
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
|
|
|
|
# Checking if BAD REQUEST (opes_pk not int or not existing)
|
|
try:
|
|
# Set pour virer les doublons
|
|
opes_post = set(map(int, filter(None, request.POST.getlist('operations[]', []))))
|
|
except ValueError:
|
|
return JsonResponse(data, status=400)
|
|
opes_all = (
|
|
Operation.objects
|
|
.select_related('group', 'group__on_acc', 'group__on_acc__negative')
|
|
.filter(pk__in=opes_post))
|
|
opes_pk = [ ope.pk for ope in opes_all ]
|
|
opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ]
|
|
if opes_notexisting:
|
|
data['errors']['opes_notexisting'] = opes_notexisting
|
|
return JsonResponse(data, status=400)
|
|
|
|
opes_already_canceled = [] # Déjà annulée
|
|
opes = [] # Pas déjà annulée
|
|
required_perms = set()
|
|
stop_all = False
|
|
cancel_duration = Settings.CANCEL_DURATION()
|
|
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
|
|
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
|
|
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
|
|
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles
|
|
for ope in opes_all:
|
|
if ope.canceled_at:
|
|
# Opération déjà annulée, va pour un warning en Response
|
|
opes_already_canceled.append(ope.pk)
|
|
else:
|
|
opes.append(ope.pk)
|
|
# Si opé il y a plus de CANCEL_DURATION, permission requise
|
|
if ope.group.at + cancel_duration < timezone.now():
|
|
required_perms.add('kfet.cancel_old_operations')
|
|
|
|
# Calcul de toutes modifs à faire en cas de validation
|
|
|
|
# Pour les balances de comptes
|
|
if not ope.group.on_acc.is_cash:
|
|
to_accounts_balances[ope.group.on_acc] -= ope.amount
|
|
if ope.addcost_for and ope.addcost_amount:
|
|
to_accounts_balances[ope.addcost_for] -= ope.addcost_amount
|
|
# Pour les groupes d'opés
|
|
to_groups_amounts[ope.group] -= ope.amount
|
|
|
|
# Pour les balances de caisses
|
|
# Les balances de caisses dont il y a eu un relevé depuis la date
|
|
# de la commande ne doivent pas être modifiées
|
|
# TODO : Prendre en compte le dernier relevé où la caisse a été
|
|
# comptée et donc modifier les balance_old (et amount_error)
|
|
# des relevés suivants.
|
|
# Note : Dans le cas où un CheckoutStatement est mis à jour
|
|
# par `.save()`, amount_error est recalculé automatiquement,
|
|
# ce qui n'est pas le cas en faisant un update sur queryset
|
|
# TODO ? : Maj les balance_old de relevés pour modifier l'erreur
|
|
last_statement = (CheckoutStatement.objects
|
|
.filter(checkout=ope.group.checkout)
|
|
.order_by('at')
|
|
.last())
|
|
if not last_statement or last_statement.at < ope.group.at:
|
|
if ope.type == Operation.PURCHASE:
|
|
if ope.group.on_acc.is_cash:
|
|
to_checkouts_balances[ope.group.checkout] -= - ope.amount
|
|
else:
|
|
to_checkouts_balances[ope.group.checkout] -= ope.amount
|
|
|
|
# Pour les stocks d'articles
|
|
# Les stocks d'articles dont il y a eu un inventaire depuis la date
|
|
# de la commande ne doivent pas être modifiés
|
|
# TODO : Prendre en compte le dernier inventaire où le stock a bien
|
|
# été compté (pas dans le cas d'une livraison).
|
|
# Note : si InventoryArticle est maj par .save(), stock_error
|
|
# est recalculé automatiquement
|
|
if ope.article and ope.article_nb:
|
|
last_stock = (InventoryArticle.objects
|
|
.select_related('inventory')
|
|
.filter(article=ope.article)
|
|
.order_by('inventory__at')
|
|
.last())
|
|
if not last_stock or last_stock.inventory.at < ope.group.at:
|
|
to_articles_stocks[ope.article] += ope.article_nb
|
|
|
|
if not opes:
|
|
data['warnings']['already_canceled'] = opes_already_canceled
|
|
return JsonResponse(data)
|
|
|
|
negative_accounts = []
|
|
# Checking permissions or stop
|
|
for account in to_accounts_balances:
|
|
(perms, stop) = account.perms_to_perform_operation(
|
|
amount = to_accounts_balances[account])
|
|
required_perms |= perms
|
|
stop_all = stop_all or stop
|
|
if stop:
|
|
negative_accounts.append(account.trigramme)
|
|
|
|
if stop_all or not request.user.has_perms(required_perms):
|
|
missing_perms = get_missing_perms(required_perms, request.user)
|
|
if missing_perms:
|
|
data['errors']['missing_perms'] = missing_perms
|
|
if stop_all:
|
|
data['errors']['negative'] = negative_accounts
|
|
return JsonResponse(data, status=403)
|
|
|
|
canceled_by = required_perms and request.user.profile.account_kfet or None
|
|
canceled_at = timezone.now()
|
|
|
|
with transaction.atomic():
|
|
(Operation.objects.filter(pk__in=opes)
|
|
.update(canceled_by=canceled_by, canceled_at=canceled_at))
|
|
for account in to_accounts_balances:
|
|
Account.objects.filter(pk=account.pk).update(
|
|
balance = F('balance') + to_accounts_balances[account])
|
|
for checkout in to_checkouts_balances:
|
|
Checkout.objects.filter(pk=checkout.pk).update(
|
|
balance = F('balance') + to_checkouts_balances[checkout])
|
|
for group in to_groups_amounts:
|
|
OperationGroup.objects.filter(pk=group.pk).update(
|
|
amount = F('amount') + to_groups_amounts[group])
|
|
for article in to_articles_stocks:
|
|
Article.objects.filter(pk=article.pk).update(
|
|
stock = F('stock') + to_articles_stocks[article])
|
|
|
|
# Websocket data
|
|
websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] }
|
|
# Need refresh from db cause we used update on querysets
|
|
opegroups_pk = [ opegroup.pk for opegroup in to_groups_amounts ]
|
|
opegroups = (OperationGroup.objects
|
|
.values('id','amount','is_cof').filter(pk__in=opegroups_pk))
|
|
for opegroup in opegroups:
|
|
websocket_data['opegroups'].append({
|
|
'cancellation': True,
|
|
'id': opegroup['id'],
|
|
'amount': opegroup['amount'],
|
|
'is_cof': opegroup['is_cof'],
|
|
})
|
|
canceled_by__trigramme = canceled_by and canceled_by.trigramme or None
|
|
for ope in opes:
|
|
websocket_data['opes'].append({
|
|
'cancellation': True,
|
|
'id': ope,
|
|
'canceled_by__trigramme': canceled_by__trigramme,
|
|
'canceled_at': canceled_at,
|
|
})
|
|
# Need refresh from db cause we used update on querysets
|
|
checkouts_pk = [ checkout.pk for checkout in to_checkouts_balances]
|
|
checkouts = (Checkout.objects
|
|
.values('id', 'balance').filter(pk__in=checkouts_pk))
|
|
for checkout in checkouts:
|
|
websocket_data['checkouts'].append({
|
|
'id': checkout['id'],
|
|
'balance': checkout['balance']})
|
|
# Need refresh from db cause we used update on querysets
|
|
articles_pk = [ article.pk for articles in to_articles_stocks]
|
|
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
|
|
for article in articles:
|
|
websocket_data['articles'].append({
|
|
'id': article['id'],
|
|
'stock': article['stock']})
|
|
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
|
|
|
|
data['canceled'] = opes
|
|
if opes_already_canceled:
|
|
data['warnings']['already_canceled'] = opes_already_canceled
|
|
return JsonResponse(data)
|
|
|
|
@login_required
|
|
def history_json(request):
|
|
# Récupération des paramètres
|
|
from_date = request.POST.get('from', None)
|
|
to_date = request.POST.get('to', None)
|
|
limit = request.POST.get('limit', None);
|
|
checkouts = request.POST.getlist('checkouts[]', None)
|
|
accounts = request.POST.getlist('accounts[]', None)
|
|
|
|
# Construction de la requête (sur les opérations) pour le prefetch
|
|
queryset_prefetch = Operation.objects.select_related(
|
|
'canceled_by__trigramme', 'addcost_for__trigramme',
|
|
'article__name')
|
|
|
|
# Construction de la requête principale
|
|
opegroups = (OperationGroup.objects
|
|
.prefetch_related(Prefetch('opes', queryset = queryset_prefetch))
|
|
.select_related('on_acc__trigramme', 'valid_by__trigramme')
|
|
.order_by('at')
|
|
)
|
|
# Application des filtres
|
|
if from_date:
|
|
opegroups = opegroups.filter(at__gte=from_date)
|
|
if to_date:
|
|
opegroups = opegroups.filter(at__lt=to_date)
|
|
if checkouts:
|
|
opegroups = opegroups.filter(checkout_id__in=checkouts)
|
|
if accounts:
|
|
opegroups = opegroups.filter(on_acc_id__in=accounts)
|
|
# Un non-membre de l'équipe n'a que accès à son historique
|
|
if not request.user.has_perm('kfet.is_team'):
|
|
opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet)
|
|
if limit:
|
|
opegroups = opegroups[:limit]
|
|
|
|
|
|
# Construction de la réponse
|
|
opegroups_list = []
|
|
for opegroup in opegroups:
|
|
opegroup_dict = {
|
|
'id' : opegroup.id,
|
|
'amount' : opegroup.amount,
|
|
'at' : opegroup.at,
|
|
'checkout_id': opegroup.checkout_id,
|
|
'is_cof' : opegroup.is_cof,
|
|
'comment' : opegroup.comment,
|
|
'opes' : [],
|
|
'on_acc__trigramme':
|
|
opegroup.on_acc and opegroup.on_acc.trigramme or None,
|
|
}
|
|
if request.user.has_perm('kfet.is_team'):
|
|
opegroup_dict['valid_by__trigramme'] = (
|
|
opegroup.valid_by and opegroup.valid_by.trigramme or None)
|
|
for ope in opegroup.opes.all():
|
|
ope_dict = {
|
|
'id' : ope.id,
|
|
'type' : ope.type,
|
|
'amount' : ope.amount,
|
|
'article_nb' : ope.article_nb,
|
|
'is_checkout' : ope.is_checkout,
|
|
'addcost_amount': ope.addcost_amount,
|
|
'canceled_at' : ope.canceled_at,
|
|
'article__name':
|
|
ope.article and ope.article.name or None,
|
|
'addcost_for__trigramme':
|
|
ope.addcost_for and ope.addcost_for.trigramme or None,
|
|
}
|
|
if request.user.has_perm('kfet.is_team'):
|
|
ope_dict['canceled_by__trigramme'] = (
|
|
ope.canceled_by and ope.canceled_by.trigramme or None)
|
|
opegroup_dict['opes'].append(ope_dict)
|
|
opegroups_list.append(opegroup_dict)
|
|
return JsonResponse({ 'opegroups': opegroups_list })
|
|
|
|
@teamkfet_required
|
|
def kpsul_articles_data(request):
|
|
articles = (
|
|
Article.objects
|
|
.values('id', 'name', 'price', 'stock', 'category_id', 'category__name')
|
|
.filter(is_sold=True))
|
|
return JsonResponse({ 'articles': list(articles) })
|
|
|
|
@teamkfet_required
|
|
def history(request):
|
|
data = {
|
|
'filter_form': FilterHistoryForm(),
|
|
'settings': {
|
|
'subvention_cof': Settings.SUBVENTION_COF(),
|
|
}
|
|
}
|
|
return render(request, 'kfet/history.html', data)
|
|
|
|
# -----
|
|
# Settings views
|
|
# -----
|
|
|
|
class SettingsList(ListView):
|
|
model = Settings
|
|
context_object_name = 'settings'
|
|
template_name = 'kfet/settings.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
Settings.create_missing()
|
|
return super(SettingsList, self).get_context_data(**kwargs)
|
|
|
|
class SettingsUpdate(SuccessMessageMixin, UpdateView):
|
|
model = Settings
|
|
form_class = SettingsForm
|
|
template_name = 'kfet/settings_update.html'
|
|
success_message = 'Paramètre %(name)s mis à jour'
|
|
success_url = reverse_lazy('kfet.settings')
|
|
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.change_settings'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
# Creating
|
|
Settings.empty_cache()
|
|
return super(SettingsUpdate, self).form_valid(form)
|
|
|
|
# -----
|
|
# Transfer views
|
|
# -----
|
|
|
|
@teamkfet_required
|
|
def transfers(request):
|
|
transfergroups = (TransferGroup.objects
|
|
.prefetch_related('transfers')
|
|
.order_by('-at'))
|
|
return render(request, 'kfet/transfers.html', {
|
|
'transfergroups': transfergroups,
|
|
})
|
|
|
|
@teamkfet_required
|
|
def transfers_create(request):
|
|
transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
|
|
return render(request, 'kfet/transfers_create.html',
|
|
{ 'transfer_formset': transfer_formset })
|
|
|
|
@teamkfet_required
|
|
def perform_transfers(request):
|
|
data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 }
|
|
|
|
# Checking transfer_formset
|
|
transfer_formset = TransferFormSet(request.POST)
|
|
if not transfer_formset.is_valid():
|
|
return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400)
|
|
|
|
transfers = transfer_formset.save(commit = False)
|
|
|
|
# Initializing vars
|
|
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers
|
|
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts
|
|
|
|
for transfer in transfers:
|
|
to_accounts_balances[transfer.from_acc] -= transfer.amount
|
|
to_accounts_balances[transfer.to_acc] += transfer.amount
|
|
|
|
stop_all = False
|
|
|
|
negative_accounts = []
|
|
# Checking if ok on all accounts
|
|
for account in to_accounts_balances:
|
|
(perms, stop) = account.perms_to_perform_operation(
|
|
amount = to_accounts_balances[account])
|
|
required_perms |= perms
|
|
stop_all = stop_all or stop
|
|
if stop:
|
|
negative_accounts.append(account.trigramme)
|
|
|
|
if stop_all or not request.user.has_perms(required_perms):
|
|
missing_perms = get_missing_perms(required_perms, request.user)
|
|
if missing_perms:
|
|
data['errors']['missing_perms'] = missing_perms
|
|
if stop_all:
|
|
data['errors']['negative'] = negative_accounts
|
|
return JsonResponse(data, status=403)
|
|
|
|
# Creating transfer group
|
|
transfergroup = TransferGroup()
|
|
if required_perms:
|
|
transfergroup.valid_by = request.user.profile.account_kfet
|
|
|
|
comment = request.POST.get('comment', '')
|
|
transfergroup.comment = comment.strip()
|
|
|
|
with transaction.atomic():
|
|
# Updating balances accounts
|
|
for account in to_accounts_balances:
|
|
Account.objects.filter(pk=account.pk).update(
|
|
balance = F('balance') + to_accounts_balances[account])
|
|
account.refresh_from_db()
|
|
if account.balance < 0:
|
|
if hasattr(account, 'negative'):
|
|
if not account.negative.start:
|
|
account.negative.start = timezone.now()
|
|
account.negative.save()
|
|
else:
|
|
negative = AccountNegative(
|
|
account = account, start = timezone.now())
|
|
negative.save()
|
|
elif (hasattr(account, 'negative')
|
|
and not account.negative.balance_offset):
|
|
account.negative.delete()
|
|
|
|
# Saving transfer group
|
|
transfergroup.save()
|
|
data['transfergroup'] = transfergroup.pk
|
|
|
|
# Saving all transfers with group
|
|
for transfer in transfers:
|
|
transfer.group = transfergroup
|
|
transfer.save()
|
|
data['transfers'].append(transfer.pk)
|
|
|
|
return JsonResponse(data)
|
|
|
|
@teamkfet_required
|
|
def cancel_transfers(request):
|
|
# Pour la réponse
|
|
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
|
|
|
|
# Checking if BAD REQUEST (transfers_pk not int or not existing)
|
|
try:
|
|
# Set pour virer les doublons
|
|
transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
|
|
except ValueError:
|
|
return JsonResponse(data, status=400)
|
|
transfers_all = (
|
|
Transfer.objects
|
|
.select_related('group', 'from_acc', 'from_acc__negative',
|
|
'to_acc', 'to_acc__negative')
|
|
.filter(pk__in=transfers_post))
|
|
transfers_pk = [ transfer.pk for transfer in transfers_all ]
|
|
transfers_notexisting = [ transfer for transfer in transfers_post
|
|
if transfer not in transfers_pk ]
|
|
if transfers_notexisting:
|
|
data['errors']['transfers_notexisting'] = transfers_notexisting
|
|
return JsonResponse(data, status=400)
|
|
|
|
transfers_already_canceled = [] # Déjà annulée
|
|
transfers = [] # Pas déjà annulée
|
|
required_perms = set()
|
|
stop_all = False
|
|
cancel_duration = Settings.CANCEL_DURATION()
|
|
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
|
|
for transfer in transfers_all:
|
|
if transfer.canceled_at:
|
|
# Transfert déjà annulé, va pour un warning en Response
|
|
transfers_already_canceled.append(transfer.pk)
|
|
else:
|
|
transfers.append(transfer.pk)
|
|
# Si transfer il y a plus de CANCEL_DURATION, permission requise
|
|
if transfer.group.at + cancel_duration < timezone.now():
|
|
required_perms.add('kfet.cancel_old_operations')
|
|
|
|
# Calcul de toutes modifs à faire en cas de validation
|
|
|
|
# Pour les balances de comptes
|
|
to_accounts_balances[transfer.from_acc] += transfer.amount
|
|
to_accounts_balances[transfer.to_acc] += -transfer.amount
|
|
|
|
if not transfers:
|
|
data['warnings']['already_canceled'] = transfers_already_canceled
|
|
return JsonResponse(data)
|
|
|
|
negative_accounts = []
|
|
# Checking permissions or stop
|
|
for account in to_accounts_balances:
|
|
(perms, stop) = account.perms_to_perform_operation(
|
|
amount = to_accounts_balances[account])
|
|
required_perms |= perms
|
|
stop_all = stop_all or stop
|
|
if stop:
|
|
negative_accounts.append(account.trigramme)
|
|
|
|
print(required_perms)
|
|
print(request.user.get_all_permissions())
|
|
|
|
if stop_all or not request.user.has_perms(required_perms):
|
|
missing_perms = get_missing_perms(required_perms, request.user)
|
|
if missing_perms:
|
|
data['errors']['missing_perms'] = missing_perms
|
|
if stop_all:
|
|
data['errors']['negative'] = negative_accounts
|
|
return JsonResponse(data, status=403)
|
|
|
|
canceled_by = required_perms and request.user.profile.account_kfet or None
|
|
canceled_at = timezone.now()
|
|
|
|
with transaction.atomic():
|
|
(Transfer.objects.filter(pk__in=transfers)
|
|
.update(canceled_by=canceled_by, canceled_at=canceled_at))
|
|
|
|
for account in to_accounts_balances:
|
|
Account.objects.filter(pk=account.pk).update(
|
|
balance = F('balance') + to_accounts_balances[account])
|
|
account.refresh_from_db()
|
|
if account.balance < 0:
|
|
if hasattr(account, 'negative'):
|
|
if not account.negative.start:
|
|
account.negative.start = timezone.now()
|
|
account.negative.save()
|
|
else:
|
|
negative = AccountNegative(
|
|
account = account, start = timezone.now())
|
|
negative.save()
|
|
elif (hasattr(account, 'negative')
|
|
and not account.negative.balance_offset):
|
|
account.negative.delete()
|
|
|
|
data['canceled'] = transfers
|
|
if transfers_already_canceled:
|
|
data['warnings']['already_canceled'] = transfers_already_canceled
|
|
return JsonResponse(data)
|
|
|
|
class InventoryList(ListView):
|
|
queryset = (Inventory.objects
|
|
.select_related('by', 'order')
|
|
.annotate(nb_articles=Count('articles'))
|
|
.order_by('-at'))
|
|
template_name = 'kfet/inventory.html'
|
|
context_object_name = 'inventories'
|
|
|
|
@teamkfet_required
|
|
def inventory_create(request):
|
|
|
|
articles = (Article.objects
|
|
.select_related('category')
|
|
.order_by('category__name', 'name')
|
|
)
|
|
initial = []
|
|
for article in articles:
|
|
initial.append({
|
|
'article' : article.pk,
|
|
'stock_old': article.stock,
|
|
'name' : article.name,
|
|
'category' : article.category_id,
|
|
'category__name': article.category.name
|
|
})
|
|
|
|
cls_formset = formset_factory(
|
|
form = InventoryArticleForm,
|
|
extra = 0,
|
|
)
|
|
|
|
if request.POST:
|
|
formset = cls_formset(request.POST, initial=initial)
|
|
|
|
if not request.user.has_perm('kfet.add_inventory'):
|
|
messages.error(request, 'Permission refusée')
|
|
elif formset.is_valid():
|
|
with transaction.atomic():
|
|
|
|
articles = Article.objects.select_for_update()
|
|
inventory = Inventory()
|
|
inventory.by = request.user.profile.account_kfet
|
|
saved = False
|
|
for form in formset:
|
|
if form.cleaned_data['stock_new'] is not None:
|
|
if not saved:
|
|
inventory.save()
|
|
saved = True
|
|
|
|
article = articles.get(pk=form.cleaned_data['article'].pk)
|
|
stock_old = article.stock
|
|
stock_new = form.cleaned_data['stock_new']
|
|
InventoryArticle.objects.create(
|
|
inventory = inventory,
|
|
article = article,
|
|
stock_old = stock_old,
|
|
stock_new = stock_new)
|
|
article.stock = stock_new
|
|
article.save()
|
|
if saved:
|
|
messages.success(request, 'Inventaire créé')
|
|
return redirect('kfet.inventory')
|
|
messages.warning(request, 'Bah alors ? On a rien compté ?')
|
|
else:
|
|
messages.error(request, 'Pas marché')
|
|
else:
|
|
formset = cls_formset(initial = initial)
|
|
|
|
return render(request, 'kfet/inventory_create.html', {
|
|
'formset': formset,
|
|
})
|
|
|
|
class InventoryRead(DetailView):
|
|
model = Inventory
|
|
template_name = 'kfet/inventory_read.html'
|
|
context_object_name = 'inventory'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(InventoryRead, self).get_context_data(**kwargs)
|
|
inventoryarticles = (InventoryArticle.objects
|
|
.select_related('article', 'article__category')
|
|
.filter(inventory = self.object)
|
|
.order_by('article__category__name', 'article__name'))
|
|
context['inventoryarts'] = inventoryarticles
|
|
return context
|
|
|
|
# -----
|
|
# Order views
|
|
# -----
|
|
|
|
class OrderList(ListView):
|
|
queryset = Order.objects.select_related('supplier', 'inventory')
|
|
template_name = 'kfet/order.html'
|
|
context_object_name = 'orders'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(OrderList, self).get_context_data(**kwargs)
|
|
context['suppliers'] = Supplier.objects.order_by('name')
|
|
return context
|
|
|
|
@teamkfet_required
|
|
def order_create(request, pk):
|
|
supplier = get_object_or_404(Supplier, pk=pk)
|
|
|
|
articles = (Article.objects
|
|
.filter(suppliers=supplier.pk)
|
|
.distinct()
|
|
.select_related('category')
|
|
.order_by('category__name', 'name'))
|
|
|
|
initial = []
|
|
today = timezone.now().date()
|
|
sales_q = (Operation.objects
|
|
.select_related('group')
|
|
.filter(article__in=articles, canceled_at=None)
|
|
.values('article'))
|
|
sales_s1 = (sales_q
|
|
.filter(
|
|
group__at__gte = today-timedelta(weeks=5),
|
|
group__at__lt = today-timedelta(weeks=4))
|
|
.annotate(nb=Sum('article_nb'))
|
|
)
|
|
sales_s1 = { d['article']:d['nb'] for d in sales_s1 }
|
|
sales_s2 = (sales_q
|
|
.filter(
|
|
group__at__gte = today-timedelta(weeks=4),
|
|
group__at__lt = today-timedelta(weeks=3))
|
|
.annotate(nb=Sum('article_nb'))
|
|
)
|
|
sales_s2 = { d['article']:d['nb'] for d in sales_s2 }
|
|
sales_s3 = (sales_q
|
|
.filter(
|
|
group__at__gte = today-timedelta(weeks=3),
|
|
group__at__lt = today-timedelta(weeks=2))
|
|
.annotate(nb=Sum('article_nb'))
|
|
)
|
|
sales_s3 = { d['article']:d['nb'] for d in sales_s3 }
|
|
sales_s4 = (sales_q
|
|
.filter(
|
|
group__at__gte = today-timedelta(weeks=2),
|
|
group__at__lt = today-timedelta(weeks=1))
|
|
.annotate(nb=Sum('article_nb'))
|
|
)
|
|
sales_s4 = { d['article']:d['nb'] for d in sales_s4 }
|
|
sales_s5 = (sales_q
|
|
.filter(group__at__gte = today-timedelta(weeks=1))
|
|
.annotate(nb=Sum('article_nb'))
|
|
)
|
|
sales_s5 = { d['article']:d['nb'] for d in sales_s5 }
|
|
|
|
for article in articles:
|
|
v_s1 = sales_s1.get(article.pk, 0)
|
|
v_s2 = sales_s2.get(article.pk, 0)
|
|
v_s3 = sales_s3.get(article.pk, 0)
|
|
v_s4 = sales_s4.get(article.pk, 0)
|
|
v_s5 = sales_s5.get(article.pk, 0)
|
|
v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
|
|
v_3max = heapq.nlargest(3, v_all)
|
|
v_moy = statistics.mean(v_3max)
|
|
v_et = statistics.pstdev(v_3max, v_moy)
|
|
v_prev = v_moy + v_et
|
|
c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
|
|
if article.box_capacity:
|
|
c_rec_temp = c_rec_tot / article.box_capacity
|
|
if c_rec_temp >= 10:
|
|
c_rec = round(c_rec_temp)
|
|
elif c_rec_temp > 5:
|
|
c_rec = 10
|
|
elif c_rec_temp > 2:
|
|
c_rec = 5
|
|
else:
|
|
c_rec = round(c_rec_temp)
|
|
initial.append({
|
|
'article': article.pk,
|
|
'name': article.name,
|
|
'category': article.category_id,
|
|
'category__name': article.category.name,
|
|
'stock': article.stock,
|
|
'box_capacity': article.box_capacity,
|
|
'v_s1': v_s1,
|
|
'v_s2': v_s2,
|
|
'v_s3': v_s3,
|
|
'v_s4': v_s4,
|
|
'v_s5': v_s5,
|
|
'v_moy': round(v_moy),
|
|
'v_et': round(v_et),
|
|
'v_prev': round(v_prev),
|
|
'c_rec': article.box_capacity and c_rec or round(c_rec_tot),
|
|
})
|
|
|
|
cls_formset = formset_factory(
|
|
form = OrderArticleForm,
|
|
extra = 0)
|
|
|
|
if request.POST:
|
|
formset = cls_formset(request.POST, initial=initial)
|
|
|
|
if not request.user.has_perm('kfet.add_order'):
|
|
messages.error(request, 'Permission refusée')
|
|
elif formset.is_valid():
|
|
order = Order()
|
|
order.supplier = supplier
|
|
saved = False
|
|
for form in formset:
|
|
if form.cleaned_data['quantity_ordered'] is not None:
|
|
if not saved:
|
|
order.save()
|
|
saved = True
|
|
|
|
article = articles.get(pk=form.cleaned_data['article'].pk)
|
|
q_ordered = form.cleaned_data['quantity_ordered']
|
|
if article.box_capacity:
|
|
q_ordered *= article.box_capacity
|
|
OrderArticle.objects.create(
|
|
order = order,
|
|
article = article,
|
|
quantity_ordered = q_ordered)
|
|
if saved:
|
|
messages.success(request, 'Commande créée')
|
|
return redirect('kfet.order.read', order.pk)
|
|
messages.warning(request, 'Rien commandé => Pas de commande')
|
|
else:
|
|
messages.error(request, 'Corrigez les erreurs')
|
|
else:
|
|
formset = cls_formset(initial=initial)
|
|
|
|
return render(request, 'kfet/order_create.html', {
|
|
'supplier': supplier,
|
|
'formset' : formset,
|
|
})
|
|
|
|
class OrderRead(DetailView):
|
|
model = Order
|
|
template_name = 'kfet/order_read.html'
|
|
context_object_name = 'order'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(OrderRead, self).get_context_data(**kwargs)
|
|
orderarticles = (OrderArticle.objects
|
|
.select_related('article', 'article__category')
|
|
.filter(order=self.object)
|
|
.order_by('article__category__name', 'article__name')
|
|
)
|
|
context['orderarts'] = orderarticles
|
|
mail = ("Bonjour,\n\nNous voudrions pour le ##DATE## à la K-Fêt de "
|
|
"l'ENS Ulm :")
|
|
category = 0
|
|
for orderarticle in orderarticles:
|
|
if category != orderarticle.article.category:
|
|
category = orderarticle.article.category
|
|
mail += '\n'
|
|
nb = orderarticle.quantity_ordered
|
|
box = ''
|
|
if orderarticle.article.box_capacity:
|
|
nb /= orderarticle.article.box_capacity
|
|
if nb >= 2:
|
|
box = ' %ss de' % orderarticle.article.box_type
|
|
else:
|
|
box = ' %s de' % orderarticle.article.box_type
|
|
name = orderarticle.article.name.capitalize()
|
|
mail += "\n- %s%s %s" % (round(nb), box, name)
|
|
|
|
mail += ("\n\nMerci d'appeler le numéro suivant lorsque les livreurs "
|
|
"sont là : ##TELEPHONE##\nCordialement,\n##PRENOM## ##NOM## "
|
|
", pour la K-Fêt de l'ENS Ulm")
|
|
|
|
context['mail'] = mail
|
|
return context
|
|
|
|
@teamkfet_required
|
|
def order_to_inventory(request, pk):
|
|
order = get_object_or_404(Order, pk=pk)
|
|
|
|
if hasattr(order, 'inventory'):
|
|
raise Http404
|
|
|
|
articles = (Article.objects
|
|
.filter(orders=order.pk)
|
|
.select_related('category')
|
|
.prefetch_related(Prefetch('orderarticle_set',
|
|
queryset = OrderArticle.objects.filter(order=order),
|
|
to_attr = 'order'))
|
|
.prefetch_related(Prefetch('supplierarticle_set',
|
|
queryset = (SupplierArticle.objects
|
|
.filter(supplier=order.supplier)
|
|
.order_by('-at')),
|
|
to_attr = 'supplier'))
|
|
.order_by('category__name', 'name'))
|
|
|
|
initial = []
|
|
for article in articles:
|
|
initial.append({
|
|
'article': article.pk,
|
|
'name': article.name,
|
|
'category': article.category_id,
|
|
'category__name': article.category.name,
|
|
'quantity_ordered': article.order[0].quantity_ordered,
|
|
'quantity_received': article.order[0].quantity_ordered,
|
|
'price_HT': article.supplier[0].price_HT,
|
|
'TVA': article.supplier[0].TVA,
|
|
'rights': article.supplier[0].rights,
|
|
})
|
|
|
|
cls_formset = formset_factory(OrderArticleToInventoryForm, extra=0)
|
|
|
|
if request.method == 'POST':
|
|
formset = cls_formset(request.POST, initial=initial)
|
|
|
|
if not request.user.has_perm('kfet.order_to_inventory'):
|
|
messages.error(request, 'Permission refusée')
|
|
elif formset.is_valid():
|
|
with transaction.atomic():
|
|
inventory = Inventory()
|
|
inventory.order = order
|
|
inventory.by = request.user.profile.account_kfet
|
|
inventory.save()
|
|
for form in formset:
|
|
q_received = form.cleaned_data['quantity_received']
|
|
article = form.cleaned_data['article']
|
|
SupplierArticle.objects.create(
|
|
supplier = order.supplier,
|
|
article = article,
|
|
price_HT = form.cleaned_data['price_HT'],
|
|
TVA = form.cleaned_data['TVA'],
|
|
rights = form.cleaned_data['rights'])
|
|
(OrderArticle.objects
|
|
.filter(order=order, article=article)
|
|
.update(quantity_received = q_received))
|
|
InventoryArticle.objects.create(
|
|
inventory = inventory,
|
|
article = article,
|
|
stock_old = article.stock,
|
|
stock_new = article.stock + q_received)
|
|
article.stock += q_received
|
|
if q_received > 0:
|
|
article.is_sold = True
|
|
article.save()
|
|
messages.success(request, "C'est tout bon !")
|
|
return redirect('kfet.order')
|
|
else:
|
|
messages.error(request, "Corrigez les erreurs")
|
|
else:
|
|
formset = cls_formset(initial=initial)
|
|
|
|
return render(request, 'kfet/order_to_inventory.html', {
|
|
'formset': formset,
|
|
})
|
|
|
|
class SupplierUpdate(SuccessMessageMixin, UpdateView):
|
|
model = Supplier
|
|
template_name = 'kfet/supplier_form.html'
|
|
fields = ['name', 'address', 'email', 'phone', 'comment']
|
|
success_url = reverse_lazy('kfet.order')
|
|
sucess_message = 'Données fournisseur mis à jour'
|
|
|
|
# Surcharge de la validation
|
|
def form_valid(self, form):
|
|
# Checking permission
|
|
if not self.request.user.has_perm('kfet.change_supplier'):
|
|
form.add_error(None, 'Permission refusée')
|
|
return self.form_invalid(form)
|
|
# Updating
|
|
return super(SupplierUpdate, self).form_valid(form)
|
|
|
|
|
|
# ==========
|
|
# 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):
|
|
"""
|
|
Summarize all the stats of an object
|
|
Handles JSONResponse
|
|
|
|
"""
|
|
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"
|
|
|
|
|
|
# Un résumé de toutes les vues ArticleStatBalance
|
|
# REND DU JSON
|
|
class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
|
|
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):
|
|
"""
|
|
Returns a JSON containing the evolution a the personnal
|
|
balance of a trigramme between timezone.now() and `nb_days`
|
|
ago (specified to the view as an argument)
|
|
takes into account the Operations and the Transfers
|
|
does not takes into account the balance offset
|
|
"""
|
|
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)
|
|
recv_transfers = Transfer.objects.filter(to_acc=account,
|
|
canceled_at=None)
|
|
sent_transfers = Transfer.objects.filter(from_acc=account,
|
|
canceled_at=None)
|
|
|
|
# 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 = [
|
|
{
|
|
'at': ope_grp.at.isoformat(),
|
|
'amount': ope_grp.amount,
|
|
'label': str(ope_grp),
|
|
'balance': 0,
|
|
} for ope_grp in opegroups
|
|
] + [
|
|
{
|
|
'at': tr.group.at.isoformat(),
|
|
'amount': tr.amount,
|
|
'label': str(tr),
|
|
'balance': 0,
|
|
} for tr in recv_transfers
|
|
] + [
|
|
{
|
|
'at': tr.group.at.isoformat(),
|
|
'amount': -tr.amount,
|
|
'label': str(tr),
|
|
'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
|
|
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
|
|
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"
|
|
|
|
|
|
# Un résumé de toutes les vues ArticleStatLast
|
|
# NE REND PAS DE JSON
|
|
class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
|
|
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):
|
|
"""
|
|
Returns a JSON containing the evolution a the personnal
|
|
consommation of a trigramme at the diffent dates specified
|
|
"""
|
|
model = Account
|
|
pk_url_kwarg = 'trigramme'
|
|
context_object_name = 'account'
|
|
id_prefix = ""
|
|
|
|
def get_operations(self, types, scale):
|
|
# 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(type__in=types)
|
|
.filter(group__on_acc=self.object)
|
|
.filter(canceled_at=None)
|
|
)
|
|
chunks = self.chunkify_qs(all_operations, scale, field='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:
|
|
nb_ventes.append(tot_ventes(chunk))
|
|
|
|
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"
|
|
|
|
|
|
# Un résumé de toutes les vues ArticleStatLast
|
|
# NE REND PAS DE JSON
|
|
class ArticleStatSalesList(SingleResumeStat):
|
|
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):
|
|
"""
|
|
Returns a JSON containing the consommation
|
|
of an article at the diffent dates precised
|
|
"""
|
|
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
|
|
|
|
# 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(type=Operation.PURCHASE,
|
|
article=self.object,
|
|
canceled_at=None,
|
|
)
|
|
)
|
|
chunks = self.chunkify_qs(all_operations, scale, field='group__at')
|
|
# On compte les opérations
|
|
nb_ventes = []
|
|
nb_accounts = []
|
|
nb_liq = []
|
|
for qs in chunks:
|
|
nb_ventes.append(
|
|
tot_ventes(qs))
|
|
nb_liq.append(
|
|
tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
|
|
nb_accounts.append(
|
|
tot_ventes(qs.exclude(group__on_acc__trigramme='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)
|