kpsul/kfet/views.py

2553 lines
97 KiB
Python
Raw Normal View History

2016-09-01 00:45:44 +02:00
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.cache import cache
from django.views.generic import ListView, DetailView
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.core.urlresolvers import reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth import authenticate, login
2016-08-02 10:40:46 +02:00
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission, Group
from django.http import HttpResponse, JsonResponse, Http404
from django.forms import modelformset_factory, formset_factory
2016-08-08 00:24:46 +02:00
from django.db import IntegrityError, transaction
from django.db.models import F, Sum, Prefetch, Count, Func
from django.db.models.functions import Coalesce
Gestion des commandes K-Psul donnant un négatif * Settings - New: OVERDRAFT_AMOUNT Découvert autorisé par défaut - New: OVERDRAFT_DURATION Durée maximum d'un découvert par défaut * K-Psul : Gestion des commandes aboutissant à un négatif - Si une commande aboutit à un nouveau solde négatif, demande la permission 'kfet.perform_negative_operations' - Si le total de la commande est négatif, vérifie que ni la contrainte de temps de découvert, ni celle de montant maximum n'est outrepassée. Si ce n'est pas le cas, la commande ne peut être enregistrée jusqu'à définir des "règles de négatif" pour le compte concerné. La durée maximum d'un découvert est celle dans AccountNegative si elle y est définie pour le compte concerné, sinon celle par défaut (Settings.OVERDRAFT_DURATION). Il en est de même pour le découvert maximum autorisé. Attention: le découvert doit être exprimé sous forme de valeur positive aussi bien dans AccountNegative que pour Settings.OVERDRAFT_AMOUNT. - Si les permissions nécessaires sont présentes, qu'il n'y a pas de blocage et que le compte n'a pas encore d'entrée dans AccountNegative, création d'une entrée avec start=now() - Si la balance d'un compte est positive après une commande, supprime l'entrée dans AccountNegative associée au compte si le "décalage de zéro" (donné par balance_offset) est nul. Sinon cela veut dire que le compte n'est pas réellement en positif. * Modèles - Fix: Account.save() fonctionne dans le cas où data est vide - Modif: AccountNegative - Valeurs par défaut, NULL...
2016-08-08 07:44:05 +02:00
from django.utils import timezone
from django.utils.crypto import get_random_string
2016-12-24 12:33:04 +01:00
from django.utils.decorators import method_decorator
2016-08-02 10:40:46 +02:00
from gestioncof.models import CofProfile, Clipper
from kfet.decorators import teamkfet_required
2016-08-11 15:14:23 +02:00
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
2016-12-24 12:33:04 +01:00
InventoryArticle, Order, OrderArticle, Operation, OperationGroup, Transfer)
from kfet.forms import *
from collections import defaultdict
from kfet import consumers
from datetime import timedelta
import django_cas_ng
2016-09-01 16:31:18 +02:00
import hashlib
import heapq
import statistics
2016-12-20 22:46:38 +01:00
from .statistic import daynames, monthnames, weeknames, \
lastdays, lastweeks, lastmonths, \
this_morning, this_monday_morning, this_first_month_day, \
tot_ventes
2016-08-02 10:40:46 +02:00
@login_required
def home(request):
2016-12-09 21:45:34 +01:00
return render(request, "kfet/home.html")
2016-08-02 10:40:46 +02:00
@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")
2016-08-02 10:40:46 +02:00
def put_cleaned_data_in_dict(dict, form):
for field in form.cleaned_data:
dict[field] = form.cleaned_data[field]
# -----
# Account views
# -----
# Account - General
2016-08-02 10:40:46 +02:00
@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):
2016-08-15 01:48:22 +02:00
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):
2016-08-02 10:40:46 +02:00
# Enregistrement
if request.method == "POST":
trigramme_form = AccountTriForm(request.POST)
2016-08-02 10:40:46 +02:00
# 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']
2016-08-02 10:40:46 +02:00
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)
2016-08-02 10:40:46 +02:00
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,
})
2016-08-02 10:40:46 +02:00
def account_form_set_readonly_fields(user_form, cof_form):
2016-08-02 10:40:46 +02:00
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):
2016-08-02 10:40:46 +02:00
user = None
clipper = None
2016-09-06 19:49:28 +02:00
if login_clipper and (login_clipper == username or not username):
2016-08-02 10:40:46 +02:00
# à partir d'un clipper
2016-08-15 01:48:22 +02:00
# le user associé à ce clipper ne devrait pas encore exister
2016-08-02 10:40:46 +02:00
clipper = get_object_or_404(Clipper, username = login_clipper)
try:
# Vérification que clipper ne soit pas déjà dans User
user = User.objects.get(username=login_clipper)
2016-08-02 10:40:46 +02:00
# Ici, on nous a menti, le user existe déjà
username = user.username
2016-09-05 08:19:28 +02:00
clipper = None
2016-08-02 10:40:46 +02:00
except User.DoesNotExist:
# Clipper (sans user déjà existant)
# UserForm - Prefill
user_initial = {
2016-08-02 10:40:46 +02:00
'username' : login_clipper,
2016-08-15 01:48:22 +02:00
'email' : "%s@clipper.ens.fr" % login_clipper}
2016-08-02 10:40:46 +02:00
if clipper.fullname:
# Prefill du nom et prénom
names = clipper.fullname.split()
# Le premier, c'est le prénom
user_initial['first_name'] = names[0]
2016-08-02 10:40:46 +02:00
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:
2016-09-01 15:03:33 +02:00
user_form = UserForm(initial=user_initial, from_clipper=True)
cof_form = CofForm(initial=cof_initial)
2016-08-02 10:40:46 +02:00
# Protection (read-only) des champs username et login_clipper
account_form_set_readonly_fields(user_form, cof_form)
2016-09-01 15:03:33 +02:00
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:
2016-08-02 10:40:46 +02:00
# 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
2016-08-02 10:40:46 +02:00
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):
forms = get_account_create_forms(request=None, username=username, login_clipper=login_clipper)
return render(request, "kfet/account_create_form.html", {
'account_form' : forms['account_form'],
'cof_form' : forms['cof_form'],
'user_form' : forms['user_form'],
2016-08-02 10:40:46 +02:00
})
# 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):
2016-08-15 01:48:22 +02:00
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)
2016-09-01 16:31:18 +02:00
pwd_form = AccountPwdForm()
2016-09-05 19:19:09 +02:00
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
2016-09-01 16:31:18 +02:00
pwd_form = None
if request.method == "POST":
# Update attempt
success = False
2016-09-05 01:24:38 +02:00
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)
2016-09-01 16:31:18 +02:00
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()):
2016-09-05 01:24:38 +02:00
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)
2016-09-01 16:31:18 +02:00
# 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']
2016-09-06 15:30:51 +02:00
pwd_sha256 = hashlib.sha256(pwd.encode('utf-8')).hexdigest()
2016-09-01 16:31:18 +02:00
Account.objects.filter(pk=account.pk).update(
password = pwd_sha256)
2016-09-01 16:31:18 +02:00
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
2016-09-05 19:19:09 +02:00
balance_offset_diff = balance_offset_new - balance_offset_old
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + balance_offset_diff)
negative_form.save()
if not balance_offset_new and Account.objects.get(pk=account.pk).balance >= 0:
AccountNegative.objects.get(account=account).delete()
success = True
messages.success(request,
'Informations du compte %s mises à jour' % account.trigramme)
if request.user == account.user:
missing_perm = False
2016-09-05 19:19:09 +02:00
account.refresh_from_db()
user_form = UserRestrictForm(request.POST, instance=account.user)
2016-09-05 19:19:09 +02:00
account_form = AccountRestrictForm(request.POST, instance=account)
if user_form.is_valid() and account_form.is_valid():
user_form.save()
account_form.save()
success = True
messages.success(request, 'Vos informations ont été mises à jour')
if missing_perm:
2016-09-03 19:41:44 +02:00
messages.error(request, 'Permission refusée')
if success:
return redirect('kfet.account.read', account.trigramme)
else:
2016-09-03 19:41:44 +02:00
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,
2016-09-01 16:31:18 +02:00
'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
2016-12-01 04:44:41 +01:00
.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
2016-12-01 04:44:41 +01:00
.exclude(account__trigramme='#13')
.aggregate(
bal = Coalesce(Sum('account__balance'),0),
offset = Coalesce(Sum('balance_offset'),0),
)
)
2016-12-01 04:39:16 +01:00
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
2016-08-15 01:48:22 +02:00
if not self.request.user.has_perm('kfet.add_checkout'):
2016-08-22 02:52:59 +02:00
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
2016-08-15 01:48:22 +02:00
if not self.request.user.has_perm('kfet.change_checkout'):
2016-08-22 02:52:59 +02:00
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(CheckoutUpdate, self).form_valid(form)
2016-08-11 15:14:23 +02:00
# -----
# 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)
2016-08-11 15:14:23 +02:00
class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
model = CheckoutStatement
template_name = 'kfet/checkoutstatement_create.html'
form_class = CheckoutStatementCreateForm
2016-08-11 15:14:23 +02:00
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)
2016-08-11 15:14:23 +02:00
# Creating
form.instance.amount_taken = getAmountTaken(form.instance)
if not form.instance.not_count:
form.instance.balance_new = getAmountBalance(form.cleaned_data)
2016-08-11 15:14:23 +02:00
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):
2016-08-30 18:16:57 +02:00
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)
2016-12-09 21:45:34 +01:00
# -----
# 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
2016-08-26 15:30:40 +02:00
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):
2016-09-05 18:09:34 +02:00
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)
2016-08-09 11:02:26 +02:00
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
2017-01-20 18:34:45 +01:00
operation.amount = Decimal(float(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:
2016-08-09 11:02:26 +02:00
required_perms.add('kfet.perform_deposit')
if (not operation.is_checkout
and operation.type in [Operation.DEPOSIT, Operation.WITHDRAW]):
required_perms.add('kfet.edit_balance_account')
need_comment = True
if operationgroup.on_acc.is_cof:
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
(perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
amount = operationgroup.amount)
required_perms |= perms
if need_comment:
operationgroup.comment = operationgroup.comment.strip()
if not operationgroup.comment:
data['errors']['need_comment'] = True
return JsonResponse(data, status=400)
if stop or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop:
data['errors']['negative'] = [operationgroup.on_acc.trigramme]
return JsonResponse(data, status=403)
# If 1 perm is required, filling who perform the operations
if required_perms:
operationgroup.valid_by = request.user.profile.account_kfet
# Filling cof status for statistics
operationgroup.is_cof = operationgroup.on_acc.is_cof
# Starting transaction to ensure data consistency
with transaction.atomic():
# If not cash account,
# saving account's balance and adding to Negative if not in
if not operationgroup.on_acc.is_cash:
Account.objects.filter(pk=operationgroup.on_acc.pk).update(
balance = F('balance') + operationgroup.amount)
operationgroup.on_acc.refresh_from_db()
if operationgroup.on_acc.balance < 0:
if hasattr(operationgroup.on_acc, 'negative'):
if not operationgroup.on_acc.negative.start:
operationgroup.on_acc.negative.start = timezone.now()
operationgroup.on_acc.negative.save()
else:
negative = AccountNegative(
account = operationgroup.on_acc, start = timezone.now())
negative.save()
elif (hasattr(operationgroup.on_acc, 'negative')
and not operationgroup.on_acc.negative.balance_offset):
operationgroup.on_acc.negative.delete()
# Updating checkout's balance
if to_checkout_balance:
Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
balance = F('balance') + to_checkout_balance)
Gestion des commandes K-Psul donnant un négatif * Settings - New: OVERDRAFT_AMOUNT Découvert autorisé par défaut - New: OVERDRAFT_DURATION Durée maximum d'un découvert par défaut * K-Psul : Gestion des commandes aboutissant à un négatif - Si une commande aboutit à un nouveau solde négatif, demande la permission 'kfet.perform_negative_operations' - Si le total de la commande est négatif, vérifie que ni la contrainte de temps de découvert, ni celle de montant maximum n'est outrepassée. Si ce n'est pas le cas, la commande ne peut être enregistrée jusqu'à définir des "règles de négatif" pour le compte concerné. La durée maximum d'un découvert est celle dans AccountNegative si elle y est définie pour le compte concerné, sinon celle par défaut (Settings.OVERDRAFT_DURATION). Il en est de même pour le découvert maximum autorisé. Attention: le découvert doit être exprimé sous forme de valeur positive aussi bien dans AccountNegative que pour Settings.OVERDRAFT_AMOUNT. - Si les permissions nécessaires sont présentes, qu'il n'y a pas de blocage et que le compte n'a pas encore d'entrée dans AccountNegative, création d'une entrée avec start=now() - Si la balance d'un compte est positive après une commande, supprime l'entrée dans AccountNegative associée au compte si le "décalage de zéro" (donné par balance_offset) est nul. Sinon cela veut dire que le compte n'est pas réellement en positif. * Modèles - Fix: Account.save() fonctionne dans le cas où data est vide - Modif: AccountNegative - Valeurs par défaut, NULL...
2016-08-08 07:44:05 +02:00
# 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)
2016-08-09 11:02:26 +02:00
@teamkfet_required
2016-08-09 11:02:26 +02:00
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[]', []))))
2016-08-09 11:02:26 +02:00
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))
2016-08-09 11:02:26 +02:00
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
2016-08-09 11:02:26 +02:00
# 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
2016-08-09 11:02:26 +02:00
# 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
2016-08-09 11:02:26 +02:00
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
2016-08-09 11:02:26 +02:00
if not opes:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)
negative_accounts = []
2016-08-09 11:02:26 +02:00
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
2016-08-26 15:30:40 +02:00
amount = to_accounts_balances[account])
2016-08-09 11:02:26 +02:00
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
2016-08-09 11:02:26 +02:00
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
2016-08-09 11:02:26 +02:00
return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None
canceled_at = timezone.now()
2016-08-09 11:02:26 +02:00
with transaction.atomic():
(Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
2016-08-09 11:02:26 +02:00
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)
2016-08-09 11:02:26 +02:00
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
2016-08-26 15:30:40 +02:00
Settings.empty_cache()
return super(SettingsUpdate, self).form_valid(form)
2016-08-26 15:30:40 +02:00
# -----
# Transfer views
# -----
@teamkfet_required
2016-08-26 20:14:00 +02:00
def transfers(request):
transfergroups = (TransferGroup.objects
.prefetch_related('transfers')
.order_by('-at'))
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
@teamkfet_required
2016-08-26 20:14:00 +02:00
def transfers_create(request):
2016-08-26 15:30:40 +02:00
transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
return render(request, 'kfet/transfers_create.html',
{ 'transfer_formset': transfer_formset })
@teamkfet_required
2016-08-26 15:30:40 +02:00
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
2016-09-05 22:12:58 +02:00
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers
2016-08-26 15:30:40 +02:00
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 = []
2016-08-26 15:30:40 +02:00
# 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)
2016-08-26 15:30:40 +02:00
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
2016-08-26 15:30:40 +02:00
return JsonResponse(data, status=403)
2016-08-26 20:14:00 +02:00
# 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()
2016-08-26 15:30:40 +02:00
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()
2016-08-26 20:14:00 +02:00
# Saving transfer group
2016-08-26 15:30:40 +02:00
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,
})
2016-08-30 23:32:54 +02:00
class InventoryRead(DetailView):
model = Inventory
template_name = 'kfet/inventory_read.html'
context_object_name = 'inventory'
def get_context_data(self, **kwargs):
context = super(InventoryRead, self).get_context_data(**kwargs)
inventoryarticles = (InventoryArticle.objects
.select_related('article', 'article__category')
.filter(inventory = self.object)
.order_by('article__category__name', 'article__name'))
context['inventoryarts'] = inventoryarticles
return context
# -----
# Order views
# -----
class OrderList(ListView):
queryset = Order.objects.select_related('supplier', 'inventory')
template_name = 'kfet/order.html'
context_object_name = 'orders'
def get_context_data(self, **kwargs):
context = super(OrderList, self).get_context_data(**kwargs)
context['suppliers'] = Supplier.objects.order_by('name')
return context
@teamkfet_required
def order_create(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
articles = (Article.objects
.filter(suppliers=supplier.pk)
.select_related('category')
.order_by('category__name', 'name'))
initial = []
today = timezone.now().date()
sales_q = (Operation.objects
.select_related('group')
.filter(article__in=articles, canceled_at=None)
.values('article'))
sales_s1 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=5),
group__at__lt = today-timedelta(weeks=4))
.annotate(nb=Sum('article_nb'))
)
sales_s1 = { d['article']:d['nb'] for d in sales_s1 }
sales_s2 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=4),
group__at__lt = today-timedelta(weeks=3))
.annotate(nb=Sum('article_nb'))
)
sales_s2 = { d['article']:d['nb'] for d in sales_s2 }
sales_s3 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=3),
group__at__lt = today-timedelta(weeks=2))
.annotate(nb=Sum('article_nb'))
)
sales_s3 = { d['article']:d['nb'] for d in sales_s3 }
sales_s4 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=2),
group__at__lt = today-timedelta(weeks=1))
.annotate(nb=Sum('article_nb'))
)
sales_s4 = { d['article']:d['nb'] for d in sales_s4 }
sales_s5 = (sales_q
.filter(group__at__gte = today-timedelta(weeks=1))
.annotate(nb=Sum('article_nb'))
)
sales_s5 = { d['article']:d['nb'] for d in sales_s5 }
for article in articles:
v_s1 = sales_s1.get(article.pk, 0)
v_s2 = sales_s2.get(article.pk, 0)
v_s3 = sales_s3.get(article.pk, 0)
v_s4 = sales_s4.get(article.pk, 0)
v_s5 = sales_s5.get(article.pk, 0)
v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
v_3max = heapq.nlargest(3, v_all)
v_moy = statistics.mean(v_3max)
v_et = statistics.pstdev(v_3max, v_moy)
v_prev = v_moy + v_et
c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
if article.box_capacity:
c_rec_temp = c_rec_tot / article.box_capacity
if c_rec_temp >= 10:
c_rec = round(c_rec_temp)
elif c_rec_temp > 5:
c_rec = 10
elif c_rec_temp > 2:
c_rec = 5
else:
c_rec = round(c_rec_temp)
initial.append({
'article': article.pk,
'name': article.name,
'category': article.category_id,
'category__name': article.category.name,
'stock': article.stock,
'box_capacity': article.box_capacity,
'v_s1': v_s1,
'v_s2': v_s2,
'v_s3': v_s3,
'v_s4': v_s4,
'v_s5': v_s5,
'v_moy': round(v_moy),
'v_et': round(v_et),
'v_prev': round(v_prev),
'c_rec': article.box_capacity and c_rec or round(c_rec_tot),
})
cls_formset = formset_factory(
form = OrderArticleForm,
extra = 0)
if request.POST:
formset = cls_formset(request.POST, initial=initial)
if not request.user.has_perm('kfet.add_order'):
messages.error(request, 'Permission refusée')
elif formset.is_valid():
order = Order()
order.supplier = supplier
saved = False
for form in formset:
if form.cleaned_data['quantity_ordered'] is not None:
if not saved:
order.save()
saved = True
article = articles.get(pk=form.cleaned_data['article'].pk)
q_ordered = form.cleaned_data['quantity_ordered']
if article.box_capacity:
q_ordered *= article.box_capacity
OrderArticle.objects.create(
order = order,
article = article,
quantity_ordered = q_ordered)
if saved:
messages.success(request, 'Commande créée')
return redirect('kfet.order.read', order.pk)
messages.warning(request, 'Rien commandé => Pas de commande')
else:
2016-09-03 19:41:44 +02:00
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)
2016-12-09 21:45:34 +01:00
2016-12-10 17:33:24 +01:00
# ==========
2016-12-09 21:45:34 +01:00
# Statistics
2016-12-10 17:33:24 +01:00
# ==========
# ---------------
# 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 HybridDetailView(JSONResponseMixin,
SingleObjectTemplateResponseMixin,
BaseDetailView):
2017-01-20 20:13:03 +01:00
"""
2017-01-20 20:27:24 +01:00
Returns a DetailView as an html page except if a JSON is requested
file by the GET method in which case it returns a JSON response.
2017-01-20 20:13:03 +01:00
"""
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format') == 'json':
return self.render_to_json_response(context)
else:
return super(HybridDetailView, self).render_to_response(context)
# Rend un ListView en html sauf si on lui précise dans
# l'appel à get que l'on veut un json auquel cas il en rend un
class HybridListView(JSONResponseMixin,
MultipleObjectTemplateResponseMixin,
BaseListView):
2017-01-20 20:27:24 +01:00
"""
Returns a ListView as an html page except if a JSON is requested
file by the GET method in which case it returns a JSON response.
"""
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format') == 'json':
return self.render_to_json_response(context)
else:
return super(HybridListView, self).render_to_response(context)
class ObjectResumeStat(DetailView):
2017-01-20 20:13:03 +01:00
"""
Summarize all the stats of an object
DOES NOT RETURN A JSON RESPONSE
"""
template_name = 'kfet/object_stat_resume.html'
context_object_name = 'lul'
id_prefix = 'id_a_definir'
# nombre de vues à résumer
nb_stat = 2
# Le combienième est celui par defaut ?
# (entre 0 et nb_stat-1)
nb_default = 0
stat_labels = ['stat_1', 'stat_2']
stat_urls = ['url_1', 'url_2']
# sert à renverser les urls
# utile de le surcharger quand l'url prend d'autres arguments que l'id
def get_object_url_kwargs(self, **kwargs):
return {'pk': self.object.id}
def get_context_data(self, **kwargs):
# On hérite pas
object_id = self.object.id
context = {}
stats = {}
for i in range(self.nb_stat):
stats[i] = {
'label': self.stat_labels[i],
'btn': "btn_%s_%d_%d" % (self.id_prefix,
object_id,
i),
'url': reverse_lazy(self.stat_urls[i],
kwargs=self.get_object_url_kwargs()),
}
prefix = "%s_%d" % (self.id_prefix, object_id)
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
2016-12-21 11:51:08 +01:00
# -----------------------
# Evolution Balance perso
# -----------------------
ID_PREFIX_ACC_BALANCE = "balance_acc"
ID_PREFIX_ACC_BALANCE_MONTH = "balance_month_acc"
ID_PREFIX_ACC_BALANCE_THREE_MONTHS = "balance_three_months_acc"
ID_PREFIX_ACC_BALANCE_SIX_MONTHS = "balance_six_months_acc"
ID_PREFIX_ACC_BALANCE_YEAR = "balance_year_acc"
ID_PREFIX_ACC_BALANCE_ANYTIME = "balance_anytime_acc"
# Un résumé de toutes les vues ArticleStatBalance
# NE REND PAS DE JSON
class AccountStatBalanceAll(ObjectResumeStat):
model = Account
context_object_name = 'account'
trigramme_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_BALANCE
nb_stat = 5
nb_default = 4
stat_labels = ["Tout le temps", "1 an", "6 mois", "3 mois", "30 jours"]
stat_urls = ['kfet.account.stat.balance.anytime',
'kfet.account.stat.balance.year',
'kfet.account.stat.balance.sixmonths',
'kfet.account.stat.balance.treemonths',
'kfet.account.stat.balance.month']
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def get_object_url_kwargs(self, **kwargs):
return {'trigramme': self.object.trigramme}
2016-12-24 12:33:04 +01:00
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
2016-12-21 11:51:08 +01:00
class AccountStatBalance(HybridDetailView):
2017-01-20 20:13:03 +01:00
"""
Returns a graph (or a JSON Response) of the evolution a the personnal
balance of a trigramm between begin_date and end_date
2017-01-20 20:27:24 +01:00
takes intto account the Operations and the Transfers
2017-01-20 20:13:03 +01:00
does not takes intto account the balance offset
"""
model = Account
trigramme_url_kwarg = 'trigramme'
template_name = 'kfet/account_stat_balance.html'
context_object_name = 'account'
begin_date = this_morning()
end_date = timezone.now() # ne gère pas encore autre chose que now
anytime = False # un cas particulier
id_prefix = "lol"
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def get_changes_list(self, **kwargs):
account = self.object
# On récupère les opérations
# TODO: retirer les opgroup dont tous les op sont annulées
opgroups = list(OperationGroup.objects
.filter(on_acc=account)
.filter(at__gte=self.begin_date)
.filter(at__lte=self.end_date))
# On récupère les transferts reçus
received_transfers = list(Transfer.objects
.filter(to_acc=account)
.filter(canceled_at=None)
.filter(group__at__gte=self.begin_date)
.filter(group__at__lte=self.end_date))
# On récupère les transferts émis
emitted_transfers = list(Transfer.objects
.filter(from_acc=account)
.filter(canceled_at=None)
.filter(group__at__gte=self.begin_date)
.filter(group__at__lte=self.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 = [
# Maintenant (à changer si on gère autre chose que now)
{
'at': self.end_date.isoformat(),
'amout': 0,
'label': "actuel",
'balance': 0,
}
] + [
{
'at': op.at.isoformat(),
'amount': op.amount,
'label': str(op),
'balance': 0,
} for op in opgroups
] + [
{
'at': tr.group.at.isoformat(),
'amount': tr.amount,
'label': "%d€: %s -> %s" % (tr.amount,
tr.from_acc.trigramme,
tr.to_acc.trigramme),
'balance': 0,
} for tr in received_transfers
] + [
{
'at': tr.group.at.isoformat(),
'amount': -tr.amount,
'label': "%d€: %s -> %s" % (tr.amount,
tr.from_acc.trigramme,
tr.to_acc.trigramme),
'balance': 0,
} for tr in emitted_transfers
]
if not self.anytime:
actions += [
# Date de début :
{
'at': self.begin_date.isoformat(),
'amount': 0,
'label': "début",
'balance': 0,
}
]
# 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, **kwargs):
context = {}
changes = self.get_changes_list()
context['changes'] = changes
context['chart_id'] = "%s_%s" % (self.id_prefix,
self.object.id)
context['min_date'] = changes[len(changes)-1]['at']
context['max_date'] = changes[0]['at']
# TODO: offset
return context
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
2016-12-21 11:51:08 +01:00
# Rend l'évolution de la balance perso de ces 30 derniers jours
class AccountStatBalanceMonth(AccountStatBalance):
begin_date = this_morning() - timezone.timedelta(days=30)
id_prefix = ID_PREFIX_ACC_BALANCE_MONTH
# Rend l'évolution de la balance perso de ces 3 derniers mois
class AccountStatBalanceThreeMonths(AccountStatBalance):
begin_date = this_morning() - timezone.timedelta(days=30*3)
id_prefix = ID_PREFIX_ACC_BALANCE_THREE_MONTHS
# Rend l'évolution de la balance perso de ces 6 derniers mois
class AccountStatBalanceSixMonths(AccountStatBalance):
begin_date = this_morning() - timezone.timedelta(days=30*6)
id_prefix = ID_PREFIX_ACC_BALANCE_SIX_MONTHS
# Rend l'évolution de la balance perso de la dernière annnée
class AccountStatBalanceYear(AccountStatBalance):
begin_date = this_morning() - timezone.timedelta(days=365)
id_prefix = ID_PREFIX_ACC_BALANCE_YEAR
# Rend l'évolution de la balance perso depuis toujours
class AccountStatBalanceAnytime(AccountStatBalance):
begin_date = timezone.datetime(year=1980, month=1, day=1)
id_prefix = ID_PREFIX_ACC_BALANCE_ANYTIME
2016-12-24 12:33:04 +01:00
anytime = True
2016-12-21 11:51:08 +01:00
2016-12-20 22:46:38 +01:00
# ------------------------
2016-12-24 12:33:04 +01:00
# Consommation personnelle
2016-12-20 22:46:38 +01:00
# ------------------------
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 AccountStatLastAll(ObjectResumeStat):
model = Account
context_object_name = 'account'
trigramme_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST
nb_stat = 3
nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
stat_urls = ['kfet.account.stat.last.month',
'kfet.account.stat.last.week',
'kfet.account.stat.last.day']
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def get_object_url_kwargs(self, **kwargs):
return {'trigramme': self.object.trigramme}
2016-12-24 12:33:04 +01:00
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
2016-12-20 22:46:38 +01:00
class AccountStatLast(HybridDetailView):
2017-01-20 20:13:03 +01:00
"""
Returns a graph (or a JSON Response) of the evolution a the personnal
2017-01-20 20:27:24 +01:00
consommation of a trigramm at the diffent dates specified
2017-01-20 20:13:03 +01:00
"""
model = Account
trigramme_url_kwarg = 'trigramme'
template_name = 'kfet/account_stat_last.html'
context_object_name = 'account'
end_date = timezone.now()
id_prefix = "lol"
# doit rendre un dictionnaire des dates
# la première date correspond au début
# la dernière date est la fin de la dernière plage
def get_dates(self, **kwargs):
2017-01-20 20:27:24 +01:00
return {}
# doit rendre un dictionnaire des labels
# le dernier label ne sera pas utilisé
def get_labels(self, **kwargs):
pass
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def sort_operations(self, **kwargs):
# On récupère les dates
dates = self.get_dates()
# On ajoute la date de fin
extended_dates = dates.copy()
extended_dates[len(dates)+1] = self.end_date
# 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='purchase')
.filter(group__on_acc=self.object)
.filter(canceled_at=None)
)
operations = {}
for i in dates:
operations[i] = (all_operations
.filter(group__at__gte=extended_dates[i])
.filter(group__at__lte=extended_dates[i+1])
)
return operations
def get_context_data(self, **kwargs):
# On hérite
# en fait non, pas besoin et c'est chiant à dumper
# context = super(AccountStat, self).get_context_data(**kwargs)
context = {}
nb_ventes = {}
# On récupère les labels des dates
context['labels'] = self.get_labels().copy()
# On compte les opérations
operations = self.sort_operations()
for i in operations:
nb_ventes[i] = tot_ventes(operations[i])
context['nb_ventes'] = nb_ventes
# ID unique
context['chart_id'] = "%s_%d" % (self.id_prefix,
self.object.id)
return context
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatLast, self).dispatch(*args, **kwargs)
2016-12-24 12:33:04 +01:00
2016-12-20 22:46:38 +01:00
# Rend les achats pour ce compte des 7 derniers jours
# Aujourd'hui non compris
class AccountStatLastDay(AccountStatLast):
end_date = this_morning()
id_prefix = ID_PREFIX_ACC_LAST_DAYS
def get_dates(self, **kwargs):
return lastdays(7)
def get_labels(self, **kwargs):
days = lastdays(7)
return daynames(days)
# Rend les achats de ce compte des 7 dernières semaines
# La semaine en cours n'est pas comprise
class AccountStatLastWeek(AccountStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ACC_LAST_WEEKS
def get_dates(self, **kwargs):
return lastweeks(7)
def get_labels(self, **kwargs):
weeks = lastweeks(7)
return weeknames(weeks)
# Rend les achats de ce compte des 7 derniers mois
# Le mois en cours n'est pas compris
class AccountStatLastMonth(AccountStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ACC_LAST_MONTHS
def get_dates(self, **kwargs):
return lastmonths(7)
def get_labels(self, **kwargs):
months = lastmonths(7)
return monthnames(months)
2016-12-10 17:33:24 +01:00
# ------------------------
# 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"
2016-12-20 22:46:38 +01:00
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
2016-12-10 17:33:24 +01:00
# Un résumé de toutes les vues ArticleStatLast
# NE REND PAS DE JSON
class ArticleStatLastAll(ObjectResumeStat):
model = Article
context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST
2016-12-20 22:46:38 +01:00
nb_stat = 3
nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
stat_urls = ['kfet.article.stat.last.month',
'kfet.article.stat.last.week',
2016-12-10 17:33:24 +01:00
'kfet.article.stat.last.day']
2016-12-09 21:45:34 +01:00
2016-12-24 12:33:04 +01:00
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
2016-12-09 21:45:34 +01:00
class ArticleStatLast(HybridDetailView):
2017-01-20 20:13:03 +01:00
"""
Returns a graph (or a JSON Response) of the consommation
of an article at the diffent dates precised
"""
model = Article
template_name = 'kfet/article_stat_last.html'
context_object_name = 'article'
end_date = timezone.now()
id_prefix = "lol"
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format') == 'json':
return self.render_to_json_response(context)
else:
return super(ArticleStatLast, self).render_to_response(context)
# doit rendre un dictionnaire des dates
# la première date correspond au début
# la dernière date est la fin de la dernière plage
def get_dates(self, **kwargs):
pass
# doit rendre un dictionnaire des labels
# le dernier label ne sera pas utilisé
def get_labels(self, **kwargs):
pass
def get_context_data(self, **kwargs):
# On hérite
# en fait non, pas besoin et c'est chiant à dumper
# context = super(ArticleStat, self).get_context_data(**kwargs)
context = {}
# On récupère les labels des dates
context['labels'] = self.get_labels().copy()
# On récupère les dates
dates = self.get_dates()
# On ajoute la date de fin
extended_dates = dates.copy()
extended_dates[len(dates)+1] = self.end_date
# 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='purchase')
.filter(article=self.object)
.filter(canceled_at=None)
)
operations = {}
for i in dates:
operations[i] = (all_operations
.filter(group__at__gte=extended_dates[i])
.filter(group__at__lte=extended_dates[i+1])
)
# On compte les opérations
nb_ventes = {}
nb_accounts = {}
nb_liq = {}
for i in operations:
nb_ventes[i] = tot_ventes(operations[i])
nb_liq[i] = tot_ventes(
operations[i]
.filter(group__on_acc__trigramme='LIQ')
)
nb_accounts[i] = tot_ventes(
operations[i]
.exclude(group__on_acc__trigramme='LIQ')
)
context['nb_ventes'] = nb_ventes
context['nb_accounts'] = nb_accounts
context['nb_liq'] = nb_liq
# ID unique
context['chart_id'] = "%s_%d" % (self.id_prefix,
self.object.id)
return context
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(ArticleStatLast, self).dispatch(*args, **kwargs)
2016-12-10 17:33:24 +01:00
# Rend les ventes des 7 derniers jours
# Aujourd'hui non compris
class ArticleStatLastDay(ArticleStatLast):
2016-12-09 21:45:34 +01:00
end_date = this_morning()
2016-12-10 17:33:24 +01:00
id_prefix = ID_PREFIX_ART_LAST_DAYS
2016-12-09 21:45:34 +01:00
def get_dates(self, **kwargs):
return lastdays(7)
def get_labels(self, **kwargs):
days = lastdays(7)
return daynames(days)
2016-12-10 17:33:24 +01:00
# Rend les ventes de 7 dernières semaines
# La semaine en cours n'est pas comprise
class ArticleStatLastWeek(ArticleStatLast):
2016-12-09 21:45:34 +01:00
end_date = this_monday_morning()
2016-12-10 17:33:24 +01:00
id_prefix = ID_PREFIX_ART_LAST_WEEKS
2016-12-09 21:45:34 +01:00
def get_dates(self, **kwargs):
return lastweeks(7)
def get_labels(self, **kwargs):
weeks = lastweeks(7)
2016-12-20 22:46:38 +01:00
return weeknames(weeks)
2016-12-24 12:33:04 +01:00
2016-12-20 22:46:38 +01:00
# Rend les ventes des 7 derniers mois
# Le mois en cours n'est pas compris
class ArticleStatLastMonth(ArticleStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ART_LAST_MONTHS
def get_dates(self, **kwargs):
return lastmonths(7)
def get_labels(self, **kwargs):
months = lastmonths(7)
return monthnames(months)