gestioCOF/kfet/views.py
2018-04-16 14:12:36 +02:00

2448 lines
89 KiB
Python

import ast
from urllib.parse import urlencode
from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied
from django.core.cache import cache
from django.views.generic import ListView, DetailView, TemplateView, FormView
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission
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.decorators import method_decorator
from gestioncof.models import CofProfile
from kfet.config import kfet_config
from kfet.decorators import teamkfet_required
from kfet.models import (
Account, Checkout, Article, AccountNegative,
CheckoutStatement, Supplier, SupplierArticle, Inventory,
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
TransferGroup, Transfer, ArticleCategory)
from kfet.forms import (
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm,
CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
TransferFormSet, InventoryArticleForm, OrderArticleForm,
OrderArticleToInventoryForm, CategoryForm, KFetConfigForm
)
from collections import defaultdict
from kfet import consumers
from datetime import timedelta
from decimal import Decimal
import heapq
import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
from .auth.views import ( # noqa
account_group, login_generic, AccountGroupCreate, AccountGroupUpdate,
)
def put_cleaned_data_in_dict(dict, form):
for field in form.cleaned_data:
dict[field] = form.cleaned_data[field]
# -----
# Account views
# -----
# Account - General
@login_required
@teamkfet_required
def account(request):
accounts = Account.objects.select_related('cofprofile__user').order_by('trigramme')
return render(request, "kfet/account.html", { 'accounts' : accounts })
@login_required
@teamkfet_required
def account_is_validandfree_ajax(request):
if not request.GET.get("trigramme", ''):
raise Http404
trigramme = request.GET.get("trigramme")
data = Account.is_validandfree(trigramme)
return JsonResponse(data)
# Account - Create
@login_required
@teamkfet_required
def account_create_special(request):
# Enregistrement
if request.method == "POST":
trigramme_form = AccountTriForm(request.POST, initial={'balance':0})
balance_form = AccountBalanceForm(request.POST)
# Peuplement des forms
username = request.POST.get('username')
login_clipper = request.POST.get('login_clipper')
forms = get_account_create_forms(
request, username=username, login_clipper=login_clipper)
account_form = forms['account_form']
cof_form = forms['cof_form']
user_form = forms['user_form']
if all((user_form.is_valid(), cof_form.is_valid(),
trigramme_form.is_valid(), account_form.is_valid(),
balance_form.is_valid())):
# Checking permission
if not request.user.has_perm('kfet.special_add_account'):
messages.error(request, 'Permission refusée')
else:
data = {}
# Fill data for Account.save()
put_cleaned_data_in_dict(data, user_form)
put_cleaned_data_in_dict(data, cof_form)
try:
account = trigramme_form.save(data = data)
account_form = AccountNoTriForm(request.POST, instance=account)
account_form.save()
balance_form = AccountBalanceForm(request.POST, instance=account)
balance_form.save()
amount = balance_form.cleaned_data['balance']
checkout = Checkout.objects.get(name='Initial')
is_cof = account.is_cof
opegroup = OperationGroup.objects.create(
on_acc=account,
checkout=checkout,
amount = amount,
is_cof = account.is_cof)
ope = Operation.objects.create(
group = opegroup,
type = Operation.INITIAL,
amount = amount)
messages.success(request, 'Compte créé : %s' % account.trigramme)
return redirect('kfet.account.create')
except Account.UserHasAccount as e:
messages.error(request, \
"Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
else:
initial = { 'trigramme': request.GET.get('trigramme', '') }
trigramme_form = AccountTriForm(initial = initial)
balance_form = AccountBalanceForm(initial = {'balance': 0})
account_form = None
cof_form = None
user_form = None
return render(request, "kfet/account_create_special.html", {
'trigramme_form': trigramme_form,
'account_form': account_form,
'cof_form': cof_form,
'user_form': user_form,
'balance_form': balance_form,
})
# Account - Create
@login_required
@teamkfet_required
def account_create(request):
# Enregistrement
if request.method == "POST":
trigramme_form = AccountTriForm(request.POST)
# Peuplement des forms
username = request.POST.get('username')
login_clipper = request.POST.get('login_clipper')
forms = get_account_create_forms(
request, username=username, login_clipper=login_clipper)
account_form = forms['account_form']
cof_form = forms['cof_form']
user_form = forms['user_form']
if all((user_form.is_valid(), cof_form.is_valid(),
trigramme_form.is_valid(), account_form.is_valid())):
# Checking permission
if not request.user.has_perm('kfet.add_account'):
messages.error(request, 'Permission refusée')
else:
data = {}
# Fill data for Account.save()
put_cleaned_data_in_dict(data, user_form)
put_cleaned_data_in_dict(data, cof_form)
try:
account = trigramme_form.save(data = data)
account_form = AccountNoTriForm(request.POST, instance=account)
account_form.save()
messages.success(request, 'Compte créé : %s' % account.trigramme)
return redirect('kfet.account.create')
except Account.UserHasAccount as e:
messages.error(request, \
"Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
else:
initial = { 'trigramme': request.GET.get('trigramme', '') }
trigramme_form = AccountTriForm(initial = initial)
account_form = None
cof_form = None
user_form = None
return render(request, "kfet/account_create.html", {
'trigramme_form': trigramme_form,
'account_form': account_form,
'cof_form': cof_form,
'user_form': user_form,
})
def account_form_set_readonly_fields(user_form, cof_form):
user_form.fields['username'].widget.attrs['readonly'] = True
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
cof_form.fields['is_cof'].widget.attrs['disabled'] = True
def get_account_create_forms(request=None, username=None, login_clipper=None,
fullname=None):
user = None
clipper = False
if login_clipper and (login_clipper == username or not username):
# à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore exister
clipper = True
try:
# Vérification que clipper ne soit pas déjà dans User
user = User.objects.get(username=login_clipper)
# Ici, on nous a menti, le user existe déjà
username = user.username
clipper = False
except User.DoesNotExist:
# Clipper (sans user déjà existant)
# UserForm - Prefill
user_initial = {
'username' : login_clipper,
'email' : "%s@clipper.ens.fr" % login_clipper}
if fullname:
# Prefill du nom et prénom
names = fullname.split()
# Le premier, c'est le prénom
user_initial['first_name'] = names[0]
if len(names) > 1:
# Si d'autres noms -> tous dans le nom de famille
user_initial['last_name'] = " ".join(names[1:])
# CofForm - Prefill
cof_initial = { 'login_clipper': login_clipper }
# Form créations
if request:
user_form = UserForm(request.POST, initial=user_initial)
cof_form = CofForm(request.POST, initial=cof_initial)
else:
user_form = UserForm(initial=user_initial)
cof_form = CofForm(initial=cof_initial)
# Protection (read-only) des champs username et login_clipper
account_form_set_readonly_fields(user_form, cof_form)
if username and not clipper:
try:
user = User.objects.get(username=username)
# le user existe déjà
# récupération du profil cof
(cof, _) = CofProfile.objects.get_or_create(user=user)
# UserForm + CofForm - Création à partir des instances existantes
if request:
user_form = UserForm(request.POST, instance = user)
cof_form = CofForm(request.POST, instance = cof)
else:
user_form = UserForm(instance=user)
cof_form = CofForm(instance=cof)
# Protection (read-only) des champs username, login_clipper et is_cof
account_form_set_readonly_fields(user_form, cof_form)
except User.DoesNotExist:
# le username donnée n'existe pas -> Création depuis rien
# (éventuellement en cours avec erreurs précédemment)
pass
if not user and not clipper:
# connaît pas du tout, faut tout remplir
if request:
user_form = UserForm(request.POST)
cof_form = CofForm(request.POST)
else:
user_form = UserForm()
cof_form = CofForm()
# mais on laisse le username en écriture
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
cof_form.fields['is_cof'].widget.attrs['disabled'] = True
if request:
account_form = AccountNoTriForm(request.POST)
else:
account_form = AccountNoTriForm()
return {
'account_form': account_form,
'cof_form': cof_form,
'user_form': user_form,
}
@login_required
@teamkfet_required
def account_create_ajax(request, username=None, login_clipper=None,
fullname=None):
forms = get_account_create_forms(
request=None, username=username, login_clipper=login_clipper,
fullname=fullname)
return render(request, "kfet/account_create_form.html", {
'account_form' : forms['account_form'],
'cof_form' : forms['cof_form'],
'user_form' : forms['user_form'],
})
# Account - Read
@login_required
def account_read(request, trigramme):
account = get_object_or_404(Account, trigramme=trigramme)
# Checking permissions
if not account.readable or (
not request.user.has_perm('kfet.is_team') and
request.user != account.user):
raise PermissionDenied
addcosts = (
OperationGroup.objects
.filter(opes__addcost_for=account,
opes__canceled_at=None)
.extra({'date': "date(at)"})
.values('date')
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
.order_by('-date')
)
return render(request, "kfet/account_read.html", {
'account': account,
'addcosts': addcosts,
})
# Account - Update
@login_required
def account_update(request, trigramme):
account = get_object_or_404(Account, trigramme=trigramme)
# Checking permissions
if not request.user.has_perm('kfet.is_team') \
and request.user != account.user:
raise PermissionDenied
if request.user.has_perm('kfet.is_team'):
user_form = UserRestrictTeamForm(instance=account.user)
group_form = UserGroupForm(instance=account.user)
account_form = AccountForm(instance=account)
cof_form = CofRestrictForm(instance=account.cofprofile)
pwd_form = AccountPwdForm()
if account.balance < 0 and not hasattr(account, 'negative'):
AccountNegative.objects.create(account=account,
start=timezone.now())
account.refresh_from_db()
if hasattr(account, 'negative'):
negative_form = AccountNegativeForm(instance=account.negative)
else:
negative_form = None
else:
user_form = UserRestrictForm(instance=account.user)
account_form = AccountRestrictForm(instance=account)
cof_form = None
group_form = None
negative_form = None
pwd_form = None
if request.method == "POST":
# Update attempt
success = False
missing_perm = True
if request.user.has_perm('kfet.is_team'):
account_form = AccountForm(request.POST, instance=account)
cof_form = CofRestrictForm(request.POST,
instance=account.cofprofile)
user_form = UserRestrictTeamForm(request.POST,
instance=account.user)
group_form = UserGroupForm(request.POST, instance=account.user)
pwd_form = AccountPwdForm(request.POST)
if hasattr(account, 'negative'):
negative_form = AccountNegativeForm(request.POST,
instance=account.negative)
if (request.user.has_perm('kfet.change_account')
and account_form.is_valid() and cof_form.is_valid()
and user_form.is_valid()):
missing_perm = False
data = {}
# Fill data for Account.save()
put_cleaned_data_in_dict(data, user_form)
put_cleaned_data_in_dict(data, cof_form)
# Updating
account_form.save(data=data)
# Checking perm to update password
if (request.user.has_perm('kfet.change_account_password')
and pwd_form.is_valid()):
pwd = pwd_form.cleaned_data['pwd1']
account.change_pwd(pwd)
account.save()
messages.success(request, 'Mot de passe mis à jour')
# Checking perm to manage perms
if (request.user.has_perm('kfet.manage_perms')
and group_form.is_valid()):
group_form.save()
# Checking perm to manage negative
if hasattr(account, 'negative'):
balance_offset_old = 0
if account.negative.balance_offset:
balance_offset_old = account.negative.balance_offset
if (hasattr(account, 'negative')
and request.user.has_perm('kfet.change_accountnegative')
and negative_form.is_valid()):
balance_offset_new = \
negative_form.cleaned_data['balance_offset']
if not balance_offset_new:
balance_offset_new = 0
balance_offset_diff = (balance_offset_new
- balance_offset_old)
Account.objects.filter(pk=account.pk).update(
balance=F('balance') + balance_offset_diff)
negative_form.save()
if Account.objects.get(pk=account.pk).balance >= 0 \
and not balance_offset_new:
AccountNegative.objects.get(account=account).delete()
success = True
messages.success(
request,
'Informations du compte %s mises à jour'
% account.trigramme)
# Modification de ses propres informations
if request.user == account.user:
missing_perm = False
account.refresh_from_db()
user_form = UserRestrictForm(request.POST, instance=account.user)
account_form = AccountRestrictForm(request.POST, instance=account)
pwd_form = AccountPwdForm(request.POST)
if user_form.is_valid() and account_form.is_valid():
user_form.save()
account_form.save()
success = True
messages.success(request,
'Vos informations ont été mises à jour')
if request.user.has_perm('kfet.is_team') \
and pwd_form.is_valid():
pwd = pwd_form.cleaned_data['pwd1']
account.change_pwd(pwd)
account.save()
messages.success(
request, 'Votre mot de passe a été mis à jour')
if missing_perm:
messages.error(request, 'Permission refusée')
if success:
return redirect('kfet.account.read', account.trigramme)
else:
messages.error(
request, 'Informations non mises à jour. Corrigez les erreurs')
return render(request, "kfet/account_update.html", {
'account': account,
'account_form': account_form,
'cof_form': cof_form,
'user_form': user_form,
'group_form': group_form,
'negative_form': negative_form,
'pwd_form': pwd_form,
})
class AccountNegativeList(ListView):
queryset = (
AccountNegative.objects
.select_related('account', 'account__cofprofile__user')
.exclude(account__trigramme='#13')
)
template_name = 'kfet/account_negative.html'
context_object_name = 'negatives'
def get_context_data(self, **kwargs):
context = super(AccountNegativeList, self).get_context_data(**kwargs)
real_balances = (neg.account.real_balance for neg in self.object_list)
context['negatives_sum'] = sum(real_balances)
return context
# -----
# Checkout views
# -----
# Checkout - General
class CheckoutList(ListView):
model = Checkout
template_name = 'kfet/checkout.html'
context_object_name = 'checkouts'
# Checkout - Create
class CheckoutCreate(SuccessMessageMixin, CreateView):
model = Checkout
template_name = 'kfet/checkout_create.html'
form_class = CheckoutForm
success_message = 'Nouvelle caisse : %(name)s'
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.add_checkout'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Creating
form.instance.created_by = self.request.user.profile.account_kfet
form.save()
return super(CheckoutCreate, self).form_valid(form)
# Checkout - Read
class CheckoutRead(DetailView):
model = Checkout
template_name = 'kfet/checkout_read.html'
context_object_name = 'checkout'
def get_context_data(self, **kwargs):
context = super(CheckoutRead, self).get_context_data(**kwargs)
context['statements'] = context['checkout'].statements.order_by('-at')
return context
# Checkout - Update
class CheckoutUpdate(SuccessMessageMixin, UpdateView):
model = Checkout
template_name = 'kfet/checkout_update.html'
form_class = CheckoutRestrictForm
success_message = 'Informations mises à jour pour la caisse : %(name)s'
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_checkout'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(CheckoutUpdate, self).form_valid(form)
# -----
# Checkout Statement views
# -----
# Checkout Statement - General
class CheckoutStatementList(ListView):
model = CheckoutStatement
queryset = CheckoutStatement.objects.order_by('-at')
template_name = 'kfet/checkoutstatement.html'
context_object_name= 'checkoutstatements'
# Checkout Statement - Create
def getAmountTaken(data):
return Decimal(data.taken_001 * 0.01 + data.taken_002 * 0.02
+ data.taken_005 * 0.05 + data.taken_01 * 0.1
+ data.taken_02 * 0.2 + data.taken_05 * 0.5
+ data.taken_1 * 1 + data.taken_2 * 2
+ data.taken_5 * 5 + data.taken_10 * 10
+ data.taken_20 * 20 + data.taken_50 * 50
+ data.taken_100 * 100 + data.taken_200 * 200
+ data.taken_500 * 500 + float(data.taken_cheque))
def getAmountBalance(data):
return Decimal(data['balance_001'] * 0.01 + data['balance_002'] * 0.02
+ data['balance_005'] * 0.05 + data['balance_01'] * 0.1
+ data['balance_02'] * 0.2 + data['balance_05'] * 0.5
+ data['balance_1'] * 1 + data['balance_2'] * 2
+ data['balance_5'] * 5 + data['balance_10'] * 10
+ data['balance_20'] * 20 + data['balance_50'] * 50
+ data['balance_100'] * 100 + data['balance_200'] * 200
+ data['balance_500'] * 500)
class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
model = CheckoutStatement
template_name = 'kfet/checkoutstatement_create.html'
form_class = CheckoutStatementCreateForm
success_message = 'Nouveau relevé : %(checkout)s - %(at)s'
def get_success_url(self):
return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})
def get_success_message(self, cleaned_data):
return self.success_message % dict(
cleaned_data,
checkout = self.object.checkout.name,
at = self.object.at)
def get_context_data(self, **kwargs):
context = super(CheckoutStatementCreate, self).get_context_data(**kwargs)
checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
context['checkout'] = checkout
return context
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.add_checkoutstatement'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Creating
form.instance.amount_taken = getAmountTaken(form.instance)
if not form.instance.not_count:
form.instance.balance_new = getAmountBalance(form.cleaned_data)
form.instance.checkout_id = self.kwargs['pk_checkout']
form.instance.by = self.request.user.profile.account_kfet
return super(CheckoutStatementCreate, self).form_valid(form)
class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
model = CheckoutStatement
template_name = 'kfet/checkoutstatement_update.html'
form_class = CheckoutStatementUpdateForm
success_message = 'Relevé modifié'
def get_success_url(self):
return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})
def get_context_data(self, **kwargs):
context = super(CheckoutStatementUpdate, self).get_context_data(**kwargs)
checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
context['checkout'] = checkout
return context
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_checkoutstatement'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
form.instance.amount_taken = getAmountTaken(form.instance)
return super(CheckoutStatementUpdate, self).form_valid(form)
# -----
# Category views
# -----
# Category - General
class CategoryList(ListView):
queryset = (ArticleCategory.objects
.prefetch_related('articles')
.order_by('name'))
template_name = 'kfet/category.html'
context_object_name = 'categories'
# Category - Update
class CategoryUpdate(SuccessMessageMixin, UpdateView):
model = ArticleCategory
template_name = 'kfet/category_update.html'
form_class = CategoryForm
success_url = reverse_lazy('kfet.category')
success_message = "Informations mises à jour pour la catégorie : %(name)s"
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_articlecategory'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(CategoryUpdate, self).form_valid(form)
# -----
# Article views
# -----
# Article - General
class ArticleList(ListView):
queryset = (
Article.objects
.select_related('category')
.prefetch_related(
Prefetch(
'inventories',
queryset=Inventory.objects.order_by('-at'),
to_attr='inventory',
)
)
.order_by('category__name', '-is_sold', 'name')
)
template_name = 'kfet/article.html'
context_object_name = 'articles'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
articles = context[self.context_object_name]
context['nb_articles'] = len(articles)
context[self.context_object_name] = articles.filter(is_sold=True)
context['not_sold_articles'] = articles.filter(is_sold=False)
return context
# Article - Create
class ArticleCreate(SuccessMessageMixin, CreateView):
model = Article
template_name = 'kfet/article_create.html'
form_class = ArticleForm
success_message = 'Nouvel item : %(category)s - %(name)s'
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.add_article'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Save ici pour save le manytomany suppliers
article = form.save()
# Save des suppliers déjà existant
for supplier in form.cleaned_data['suppliers']:
SupplierArticle.objects.create(
article=article, supplier=supplier)
# Nouveau supplier
supplier_new = form.cleaned_data['supplier_new'].strip()
if supplier_new:
supplier, created = Supplier.objects.get_or_create(
name=supplier_new)
if created:
SupplierArticle.objects.create(
article=article, supplier=supplier)
# Inventaire avec stock initial
inventory = Inventory()
inventory.by = self.request.user.profile.account_kfet
inventory.save()
InventoryArticle.objects.create(
inventory=inventory,
article=article,
stock_old=article.stock,
stock_new=article.stock,
)
# Creating
return super(ArticleCreate, self).form_valid(form)
# Article - Read
class ArticleRead(DetailView):
model = Article
template_name = 'kfet/article_read.html'
context_object_name = 'article'
def get_context_data(self, **kwargs):
context = super(ArticleRead, self).get_context_data(**kwargs)
inventoryarts = (InventoryArticle.objects
.filter(article=self.object)
.select_related('inventory')
.order_by('-inventory__at'))
context['inventoryarts'] = inventoryarts
supplierarts = (SupplierArticle.objects
.filter(article=self.object)
.select_related('supplier')
.order_by('-at'))
context['supplierarts'] = supplierarts
return context
# Article - Update
class ArticleUpdate(SuccessMessageMixin, UpdateView):
model = Article
template_name = 'kfet/article_update.html'
form_class = ArticleRestrictForm
success_message = "Informations mises à jour pour l'article : %(name)s"
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_article'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Save ici pour save le manytomany suppliers
article = form.save()
# Save des suppliers déjà existant
for supplier in form.cleaned_data['suppliers']:
if supplier not in article.suppliers.all():
SupplierArticle.objects.create(
article=article, supplier=supplier)
# On vire les suppliers désélectionnés
for supplier in article.suppliers.all():
if supplier not in form.cleaned_data['suppliers']:
SupplierArticle.objects.filter(
article=article, supplier=supplier).delete()
# Nouveau supplier
supplier_new = form.cleaned_data['supplier_new'].strip()
if supplier_new:
supplier, created = Supplier.objects.get_or_create(
name=supplier_new)
if created:
SupplierArticle.objects.create(
article=article, supplier=supplier)
# Updating
return super(ArticleUpdate, self).form_valid(form)
# -----
# K-Psul
# -----
@teamkfet_required
def kpsul(request):
data = {}
data['operationgroup_form'] = KPsulOperationGroupForm()
data['trigramme_form'] = KPsulAccountForm()
data['checkout_form'] = KPsulCheckoutForm()
data['operation_formset'] = KPsulOperationFormSet(
queryset=Operation.objects.none(),
)
return render(request, 'kfet/kpsul.html', data)
@teamkfet_required
def kpsul_get_settings(request):
addcost_for = kfet_config.addcost_for
data = {
'subvention_cof': kfet_config.subvention_cof,
'addcost_for': addcost_for and addcost_for.trigramme or '',
'addcost_amount': kfet_config.addcost_amount,
}
return JsonResponse(data)
@teamkfet_required
def account_read_json(request):
trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme)
data = { 'id': account.pk, 'name': account.name, 'email': account.email,
'is_cof': account.is_cof, 'promo': account.promo,
'balance': account.balance, 'is_frozen': account.is_frozen,
'departement': account.departement, 'nickname': account.nickname,
'trigramme': account.trigramme }
return JsonResponse(data)
@teamkfet_required
def kpsul_checkout_data(request):
pk = request.POST.get('pk', 0)
if not pk:
pk = 0
data = (
Checkout.objects
.annotate(
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'),
last_statement_by_trigramme=F('statements__by__trigramme'),
last_statement_balance=F('statements__balance_new'),
last_statement_at=F('statements__at'))
.select_related(
'statements'
'statements__by',
'statements__by__cofprofile__user')
.filter(pk=pk)
.order_by('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')
.last()
)
if data is None:
raise Http404
return JsonResponse(data)
@teamkfet_required
def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST)
if not addcost_form.is_valid():
data = {'errors': {'addcost': list(addcost_form.errors)}}
return JsonResponse(data, status=400)
required_perms = ['kfet.manage_addcosts']
if not request.user.has_perms(required_perms):
data = {
'errors': {
'missing_perms': get_missing_perms(required_perms,
request.user)
}
}
return JsonResponse(data, status=403)
trigramme = addcost_form.cleaned_data['trigramme']
account = trigramme and Account.objects.get(trigramme=trigramme) or None
amount = addcost_form.cleaned_data['amount']
kfet_config.set(addcost_for=account,
addcost_amount=amount)
data = {
'addcost': {
'for': account and account.trigramme or None,
'amount': amount,
}
}
consumers.KPsul.group_send('kfet.kpsul', data)
return JsonResponse(data)
def get_missing_perms(required_perms, user):
missing_perms_codenames = [(perm.split('.'))[1]
for perm in required_perms
if not user.has_perm(perm)]
missing_perms = list(
Permission.objects
.filter(codename__in=missing_perms_codenames)
.values_list('name', flat=True)
)
return missing_perms
@teamkfet_required
def kpsul_perform_operations(request):
# Initializing response data
data = {'operationgroup': 0, 'operations': [],
'warnings': {}, 'errors': {}}
# Checking operationgroup
operationgroup_form = KPsulOperationGroupForm(request.POST)
if not operationgroup_form.is_valid():
data['errors']['operation_group'] = list(operationgroup_form.errors)
# Checking operation_formset
operation_formset = KPsulOperationFormSet(request.POST)
if not operation_formset.is_valid():
data['errors']['operations'] = list(operation_formset.errors)
# Returning BAD REQUEST if errors
if data['errors']:
return JsonResponse(data, status=400)
# Pre-saving (no commit)
operationgroup = operationgroup_form.save(commit=False)
operations = operation_formset.save(commit=False)
# Retrieving COF grant
cof_grant = kfet_config.subvention_cof
# Retrieving addcosts data
addcost_amount = kfet_config.addcost_amount
addcost_for = kfet_config.addcost_for
# Initializing vars
required_perms = set() # Required perms to perform all operations
cof_grant_divisor = 1 + cof_grant / 100
to_addcost_for_balance = 0 # For balance of addcost_for
to_checkout_balance = 0 # For balance of selected checkout
to_articles_stocks = defaultdict(lambda: 0) # For stocks articles
is_addcost = all((addcost_for, addcost_amount,
addcost_for != operationgroup.on_acc))
need_comment = operationgroup.on_acc.need_comment
# Filling data of each operations
# + operationgroup + calculating other stuffs
for operation in operations:
if operation.type == Operation.PURCHASE:
operation.amount = - operation.article.price * operation.article_nb
if is_addcost & operation.article.category.has_addcost:
operation.addcost_for = addcost_for
operation.addcost_amount = addcost_amount \
* operation.article_nb
operation.amount -= operation.addcost_amount
to_addcost_for_balance += operation.addcost_amount
if operationgroup.on_acc.is_cash:
to_checkout_balance += -operation.amount
if operationgroup.on_acc.is_cof:
if is_addcost and operation.article.category.has_addcost:
operation.addcost_amount /= cof_grant_divisor
operation.amount = operation.amount / cof_grant_divisor
to_articles_stocks[operation.article] -= operation.article_nb
else:
if operationgroup.on_acc.is_cash:
data['errors']['account'] = 'LIQ'
if operation.type != Operation.EDIT:
to_checkout_balance += operation.amount
operationgroup.amount += operation.amount
if operation.type == Operation.DEPOSIT:
required_perms.add('kfet.perform_deposit')
if operation.type == Operation.EDIT:
required_perms.add('kfet.edit_balance_account')
need_comment = True
if operationgroup.on_acc.is_cof:
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
(perms, stop) = (operationgroup.on_acc
.perms_to_perform_operation(
amount=operationgroup.amount)
)
required_perms |= perms
if need_comment:
operationgroup.comment = operationgroup.comment.strip()
if not operationgroup.comment:
data['errors']['need_comment'] = True
if data['errors']:
return JsonResponse(data, status=400)
if stop or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop:
data['errors']['negative'] = [operationgroup.on_acc.trigramme]
return JsonResponse(data, status=403)
# If 1 perm is required, filling who perform the operations
if required_perms:
operationgroup.valid_by = request.user.profile.account_kfet
# Filling cof status for statistics
operationgroup.is_cof = operationgroup.on_acc.is_cof
# Starting transaction to ensure data consistency
with transaction.atomic():
# If not cash account,
# saving account's balance and adding to Negative if not in
on_acc = operationgroup.on_acc
if not on_acc.is_cash:
(
Account.objects
.filter(pk=on_acc.pk)
.update(balance=F('balance') + operationgroup.amount)
)
on_acc.refresh_from_db()
on_acc.update_negative()
# Updating checkout's balance
if to_checkout_balance:
Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
balance=F('balance') + to_checkout_balance)
# Saving addcost_for with new balance if there is one
if is_addcost and to_addcost_for_balance:
Account.objects.filter(pk=addcost_for.pk).update(
balance=F('balance') + to_addcost_for_balance)
# Saving operation group
operationgroup.save()
data['operationgroup'] = operationgroup.pk
# Filling operationgroup id for each operations and saving
for operation in operations:
operation.group = operationgroup
operation.save()
data['operations'].append(operation.pk)
# Updating articles stock
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
stock=F('stock') + to_articles_stocks[article])
# Websocket data
websocket_data = {}
websocket_data['opegroups'] = [{
'add': True,
'id': operationgroup.pk,
'amount': operationgroup.amount,
'checkout__name': operationgroup.checkout.name,
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
'valid_by__trigramme': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme,
'opes': [],
}]
for operation in operations:
ope_data = {
'id': operation.pk, 'type': operation.type,
'amount': operation.amount,
'addcost_amount': operation.addcost_amount,
'addcost_for__trigramme': (
operation.addcost_for and addcost_for.trigramme or None),
'article__name': (
operation.article and operation.article.name or None),
'article_nb': operation.article_nb,
'group_id': operationgroup.pk,
'canceled_by__trigramme': None, 'canceled_at': None,
}
websocket_data['opegroups'][0]['opes'].append(ope_data)
# Need refresh from db cause we used update on queryset
operationgroup.checkout.refresh_from_db()
websocket_data['checkouts'] = [{
'id': operationgroup.checkout.pk,
'balance': operationgroup.checkout.balance,
}]
websocket_data['articles'] = []
# Need refresh from db cause we used update on querysets
articles_pk = [article.pk for article in to_articles_stocks]
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
for article in articles:
websocket_data['articles'].append({
'id': article['id'],
'stock': article['stock']
})
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
return JsonResponse(data)
@teamkfet_required
def kpsul_cancel_operations(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (opes_pk not int or not existing)
try:
# Set pour virer les doublons
opes_post = set(map(int, filter(None, request.POST.getlist('operations[]', []))))
except ValueError:
return JsonResponse(data, status=400)
opes_all = (
Operation.objects
.select_related('group', 'group__on_acc', 'group__on_acc__negative')
.filter(pk__in=opes_post))
opes_pk = [ ope.pk for ope in opes_all ]
opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ]
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
return JsonResponse(data, status=400)
opes_already_canceled = [] # Déjà annulée
opes = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles
for ope in opes_all:
if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response
opes_already_canceled.append(ope.pk)
else:
opes.append(ope.pk)
# Si opé il y a plus de CANCEL_DURATION, permission requise
if ope.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
if not ope.group.on_acc.is_cash:
to_accounts_balances[ope.group.on_acc] -= ope.amount
if ope.addcost_for and ope.addcost_amount:
to_accounts_balances[ope.addcost_for] -= ope.addcost_amount
# Pour les groupes d'opés
to_groups_amounts[ope.group] -= ope.amount
# Pour les balances de caisses
# Les balances de caisses dont il y a eu un relevé depuis la date
# de la commande ne doivent pas être modifiées
# TODO : Prendre en compte le dernier relevé où la caisse a été
# comptée et donc modifier les balance_old (et amount_error)
# des relevés suivants.
# Note : Dans le cas où un CheckoutStatement est mis à jour
# par `.save()`, amount_error est recalculé automatiquement,
# ce qui n'est pas le cas en faisant un update sur queryset
# TODO ? : Maj les balance_old de relevés pour modifier l'erreur
last_statement = (CheckoutStatement.objects
.filter(checkout=ope.group.checkout)
.order_by('at')
.last())
if not last_statement or last_statement.at < ope.group.at:
if ope.is_checkout:
if ope.group.on_acc.is_cash:
to_checkouts_balances[ope.group.checkout] -= - ope.amount
else:
to_checkouts_balances[ope.group.checkout] -= ope.amount
# Pour les stocks d'articles
# Les stocks d'articles dont il y a eu un inventaire depuis la date
# de la commande ne doivent pas être modifiés
# TODO : Prendre en compte le dernier inventaire où le stock a bien
# été compté (pas dans le cas d'une livraison).
# Note : si InventoryArticle est maj par .save(), stock_error
# est recalculé automatiquement
if ope.article and ope.article_nb:
last_stock = (InventoryArticle.objects
.select_related('inventory')
.filter(article=ope.article)
.order_by('inventory__at')
.last())
if not last_stock or last_stock.inventory.at < ope.group.at:
to_articles_stocks[ope.article] += ope.article_nb
if not opes:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)
negative_accounts = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = negative_accounts
return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None
canceled_at = timezone.now()
with transaction.atomic():
(Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
(
Account.objects
.filter(pk=account.pk)
.update(balance=F('balance') + to_accounts_balances[account])
)
if not account.is_cash:
# Should always be true, but we want to be sure
account.refresh_from_db()
account.update_negative()
for checkout in to_checkouts_balances:
Checkout.objects.filter(pk=checkout.pk).update(
balance = F('balance') + to_checkouts_balances[checkout])
for group in to_groups_amounts:
OperationGroup.objects.filter(pk=group.pk).update(
amount = F('amount') + to_groups_amounts[group])
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
stock = F('stock') + to_articles_stocks[article])
# Websocket data
websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] }
# Need refresh from db cause we used update on querysets
opegroups_pk = [ opegroup.pk for opegroup in to_groups_amounts ]
opegroups = (OperationGroup.objects
.values('id','amount','is_cof').filter(pk__in=opegroups_pk))
for opegroup in opegroups:
websocket_data['opegroups'].append({
'cancellation': True,
'id': opegroup['id'],
'amount': opegroup['amount'],
'is_cof': opegroup['is_cof'],
})
canceled_by__trigramme = canceled_by and canceled_by.trigramme or None
for ope in opes:
websocket_data['opes'].append({
'cancellation': True,
'id': ope,
'canceled_by__trigramme': canceled_by__trigramme,
'canceled_at': canceled_at,
})
# Need refresh from db cause we used update on querysets
checkouts_pk = [ checkout.pk for checkout in to_checkouts_balances]
checkouts = (Checkout.objects
.values('id', 'balance').filter(pk__in=checkouts_pk))
for checkout in checkouts:
websocket_data['checkouts'].append({
'id': checkout['id'],
'balance': checkout['balance']})
# Need refresh from db cause we used update on querysets
articles_pk = [ article.pk for articles in to_articles_stocks]
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
for article in articles:
websocket_data['articles'].append({
'id': article['id'],
'stock': article['stock']})
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
data['canceled'] = opes
if opes_already_canceled:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)
@login_required
def history_json(request):
# Récupération des paramètres
from_date = request.POST.get('from', None)
to_date = request.POST.get('to', None)
limit = request.POST.get('limit', None);
checkouts = request.POST.getlist('checkouts[]', None)
accounts = request.POST.getlist('accounts[]', None)
# Construction de la requête (sur les opérations) pour le prefetch
queryset_prefetch = Operation.objects.select_related(
'article', 'canceled_by', 'addcost_for')
# Construction de la requête principale
opegroups = (
OperationGroup.objects
.prefetch_related(Prefetch('opes', queryset=queryset_prefetch))
.select_related('on_acc', 'valid_by')
.order_by('at')
)
# Application des filtres
if from_date:
opegroups = opegroups.filter(at__gte=from_date)
if to_date:
opegroups = opegroups.filter(at__lt=to_date)
if checkouts:
opegroups = opegroups.filter(checkout_id__in=checkouts)
if accounts:
opegroups = opegroups.filter(on_acc_id__in=accounts)
# Un non-membre de l'équipe n'a que accès à son historique
if not request.user.has_perm('kfet.is_team'):
opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet)
if limit:
opegroups = opegroups[:limit]
# Construction de la réponse
opegroups_list = []
for opegroup in opegroups:
opegroup_dict = {
'id' : opegroup.id,
'amount' : opegroup.amount,
'at' : opegroup.at,
'checkout_id': opegroup.checkout_id,
'is_cof' : opegroup.is_cof,
'comment' : opegroup.comment,
'opes' : [],
'on_acc__trigramme':
opegroup.on_acc and opegroup.on_acc.trigramme or None,
}
if request.user.has_perm('kfet.is_team'):
opegroup_dict['valid_by__trigramme'] = (
opegroup.valid_by and opegroup.valid_by.trigramme or None)
for ope in opegroup.opes.all():
ope_dict = {
'id' : ope.id,
'type' : ope.type,
'amount' : ope.amount,
'article_nb' : ope.article_nb,
'addcost_amount': ope.addcost_amount,
'canceled_at' : ope.canceled_at,
'article__name':
ope.article and ope.article.name or None,
'addcost_for__trigramme':
ope.addcost_for and ope.addcost_for.trigramme or None,
}
if request.user.has_perm('kfet.is_team'):
ope_dict['canceled_by__trigramme'] = (
ope.canceled_by and ope.canceled_by.trigramme or None)
opegroup_dict['opes'].append(ope_dict)
opegroups_list.append(opegroup_dict)
return JsonResponse({ 'opegroups': opegroups_list })
@teamkfet_required
def kpsul_articles_data(request):
articles = (
Article.objects
.values('id', 'name', 'price', 'stock', 'category_id',
'category__name', 'category__has_addcost')
.filter(is_sold=True))
return JsonResponse({ 'articles': list(articles) })
@teamkfet_required
def history(request):
data = {
'filter_form': FilterHistoryForm(),
}
return render(request, 'kfet/history.html', data)
# -----
# Settings views
# -----
class SettingsList(TemplateView):
template_name = 'kfet/settings.html'
config_list = permission_required('kfet.see_config')(SettingsList.as_view())
class SettingsUpdate(SuccessMessageMixin, FormView):
form_class = KFetConfigForm
template_name = 'kfet/settings_update.html'
success_message = 'Paramètres mis à jour'
success_url = reverse_lazy('kfet.settings')
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_config'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
form.save()
return super().form_valid(form)
config_update = (
permission_required('kfet.change_config')(SettingsUpdate.as_view())
)
# -----
# Transfer views
# -----
@teamkfet_required
def transfers(request):
transfers_pre = Prefetch(
'transfers',
queryset=(
Transfer.objects
.select_related('from_acc', 'to_acc')
),
)
transfergroups = (
TransferGroup.objects
.select_related('valid_by')
.prefetch_related(transfers_pre)
.order_by('-at')
)
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
@teamkfet_required
def transfers_create(request):
transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
return render(request, 'kfet/transfers_create.html',
{ 'transfer_formset': transfer_formset })
@teamkfet_required
def perform_transfers(request):
data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 }
# Checking transfer_formset
transfer_formset = TransferFormSet(request.POST)
if not transfer_formset.is_valid():
return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400)
transfers = transfer_formset.save(commit = False)
# Initializing vars
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts
for transfer in transfers:
to_accounts_balances[transfer.from_acc] -= transfer.amount
to_accounts_balances[transfer.to_acc] += transfer.amount
stop_all = False
negative_accounts = []
# Checking if ok on all accounts
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = negative_accounts
return JsonResponse(data, status=403)
# Creating transfer group
transfergroup = TransferGroup()
if required_perms:
transfergroup.valid_by = request.user.profile.account_kfet
comment = request.POST.get('comment', '')
transfergroup.comment = comment.strip()
with transaction.atomic():
# Updating balances accounts
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
if not account.negative.start:
account.negative.start = timezone.now()
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
account.negative.delete()
# Saving transfer group
transfergroup.save()
data['transfergroup'] = transfergroup.pk
# Saving all transfers with group
for transfer in transfers:
transfer.group = transfergroup
transfer.save()
data['transfers'].append(transfer.pk)
return JsonResponse(data)
@teamkfet_required
def cancel_transfers(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (transfers_pk not int or not existing)
try:
# Set pour virer les doublons
transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
except ValueError:
return JsonResponse(data, status=400)
transfers_all = (
Transfer.objects
.select_related('group', 'from_acc', 'from_acc__negative',
'to_acc', 'to_acc__negative')
.filter(pk__in=transfers_post))
transfers_pk = [ transfer.pk for transfer in transfers_all ]
transfers_notexisting = [ transfer for transfer in transfers_post
if transfer not in transfers_pk ]
if transfers_notexisting:
data['errors']['transfers_notexisting'] = transfers_notexisting
return JsonResponse(data, status=400)
transfers_already_canceled = [] # Déjà annulée
transfers = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
for transfer in transfers_all:
if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response
transfers_already_canceled.append(transfer.pk)
else:
transfers.append(transfer.pk)
# Si transfer il y a plus de CANCEL_DURATION, permission requise
if transfer.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
to_accounts_balances[transfer.from_acc] += transfer.amount
to_accounts_balances[transfer.to_acc] += -transfer.amount
if not transfers:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
negative_accounts = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = negative_accounts
return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None
canceled_at = timezone.now()
with transaction.atomic():
(Transfer.objects.filter(pk__in=transfers)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
if not account.negative.start:
account.negative.start = timezone.now()
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
account.negative.delete()
data['canceled'] = transfers
if transfers_already_canceled:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
class InventoryList(ListView):
queryset = (Inventory.objects
.select_related('by', 'order')
.annotate(nb_articles=Count('articles'))
.order_by('-at'))
template_name = 'kfet/inventory.html'
context_object_name = 'inventories'
@teamkfet_required
def inventory_create(request):
articles = (Article.objects
.select_related('category')
.order_by('category__name', 'name')
)
initial = []
for article in articles:
initial.append({
'article' : article.pk,
'stock_old': article.stock,
'name' : article.name,
'category' : article.category_id,
'category__name': article.category.name,
'box_capacity': article.box_capacity or 0,
})
cls_formset = formset_factory(
form = InventoryArticleForm,
extra = 0,
)
if request.POST:
formset = cls_formset(request.POST, initial=initial)
if not request.user.has_perm('kfet.add_inventory'):
messages.error(request, 'Permission refusée')
elif formset.is_valid():
with transaction.atomic():
articles = Article.objects.select_for_update()
inventory = Inventory()
inventory.by = request.user.profile.account_kfet
saved = False
for form in formset:
if form.cleaned_data['stock_new'] is not None:
if not saved:
inventory.save()
saved = True
article = articles.get(pk=form.cleaned_data['article'].pk)
stock_old = article.stock
stock_new = form.cleaned_data['stock_new']
InventoryArticle.objects.create(
inventory = inventory,
article = article,
stock_old = stock_old,
stock_new = stock_new)
article.stock = stock_new
article.save()
if saved:
messages.success(request, 'Inventaire créé')
return redirect('kfet.inventory')
messages.warning(request, 'Bah alors ? On a rien compté ?')
else:
messages.error(request, 'Pas marché')
else:
formset = cls_formset(initial = initial)
return render(request, 'kfet/inventory_create.html', {
'formset': formset,
})
class InventoryRead(DetailView):
model = Inventory
template_name = 'kfet/inventory_read.html'
context_object_name = 'inventory'
def get_context_data(self, **kwargs):
context = super(InventoryRead, self).get_context_data(**kwargs)
inventoryarticles = (InventoryArticle.objects
.select_related('article', 'article__category')
.filter(inventory = self.object)
.order_by('article__category__name', 'article__name'))
context['inventoryarts'] = inventoryarticles
return context
# -----
# Order views
# -----
class OrderList(ListView):
queryset = Order.objects.select_related('supplier', 'inventory')
template_name = 'kfet/order.html'
context_object_name = 'orders'
def get_context_data(self, **kwargs):
context = super(OrderList, self).get_context_data(**kwargs)
context['suppliers'] = Supplier.objects.order_by('name')
return context
@teamkfet_required
def order_create(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
articles = (
Article.objects
.filter(suppliers=supplier.pk)
.distinct()
.select_related('category')
.order_by('category__name', 'name')
)
# Force hit to cache
articles = list(articles)
sales_q = (
Operation.objects
.select_related('group')
.filter(article__in=articles, canceled_at=None)
.values('article')
.annotate(nb=Sum('article_nb'))
)
scale = WeekScale(last=True, n_steps=5, std_chunk=False)
chunks = scale.chunkify_qs(sales_q, field='group__at')
sales = [
{d['article']: d['nb'] for d in chunk}
for chunk in chunks
]
initial = []
for article in articles:
# Get sales for each 5 last weeks
v_all = [chunk.get(article.pk, 0) for chunk in sales]
# Take the 3 greatest (eg to avoid 2 weeks of vacations)
v_3max = heapq.nlargest(3, v_all)
# Get average and standard deviation
v_moy = statistics.mean(v_3max)
v_et = statistics.pstdev(v_3max, v_moy)
# Expected sales for next week
v_prev = v_moy + v_et
# We want to have 1.5 * the expected sales in stock
# (because sometimes some articles are not delivered)
c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
# If ordered quantity is close enough to a level which can led to free
# boxes, we increase it to this level.
if article.box_capacity:
c_rec_temp = c_rec_tot / article.box_capacity
if c_rec_temp >= 10:
c_rec = round(c_rec_temp)
elif c_rec_temp > 5:
c_rec = 10
elif c_rec_temp > 2:
c_rec = 5
else:
c_rec = round(c_rec_temp)
initial.append({
'article': article.pk,
'name': article.name,
'category': article.category_id,
'category__name': article.category.name,
'stock': article.stock,
'box_capacity': article.box_capacity,
'v_all': v_all,
'v_moy': round(v_moy),
'v_et': round(v_et),
'v_prev': round(v_prev),
'c_rec': article.box_capacity and c_rec or round(c_rec_tot),
})
cls_formset = formset_factory(
form=OrderArticleForm,
extra=0,
)
if request.POST:
formset = cls_formset(request.POST, initial=initial)
if not request.user.has_perm('kfet.add_order'):
messages.error(request, 'Permission refusée')
elif formset.is_valid():
order = Order()
order.supplier = supplier
saved = False
for form in formset:
if form.cleaned_data['quantity_ordered'] is not None:
if not saved:
order.save()
saved = True
article = form.cleaned_data['article']
q_ordered = form.cleaned_data['quantity_ordered']
if article.box_capacity:
q_ordered *= article.box_capacity
OrderArticle.objects.create(
order=order,
article=article,
quantity_ordered=q_ordered,
)
if saved:
messages.success(request, 'Commande créée')
return redirect('kfet.order.read', order.pk)
messages.warning(request, 'Rien commandé => Pas de commande')
else:
messages.error(request, 'Corrigez les erreurs')
else:
formset = cls_formset(initial=initial)
scale.label_fmt = "S-{rev_i}"
return render(request, 'kfet/order_create.html', {
'supplier': supplier,
'formset': formset,
'scale': scale,
})
class OrderRead(DetailView):
model = Order
template_name = 'kfet/order_read.html'
context_object_name = 'order'
def get_context_data(self, **kwargs):
context = super(OrderRead, self).get_context_data(**kwargs)
orderarticles = (OrderArticle.objects
.select_related('article', 'article__category')
.filter(order=self.object)
.order_by('article__category__name', 'article__name')
)
context['orderarts'] = orderarticles
mail = ("Bonjour,\n\nNous voudrions pour le ##DATE## à la K-Fêt de "
"l'ENS Ulm :")
category = 0
for orderarticle in orderarticles:
if category != orderarticle.article.category:
category = orderarticle.article.category
mail += '\n'
nb = orderarticle.quantity_ordered
box = ''
if orderarticle.article.box_capacity:
nb /= orderarticle.article.box_capacity
if nb >= 2:
box = ' %ss de' % orderarticle.article.box_type
else:
box = ' %s de' % orderarticle.article.box_type
name = orderarticle.article.name.capitalize()
mail += "\n- %s%s %s" % (round(nb), box, name)
mail += ("\n\nMerci d'appeler le numéro suivant lorsque les livreurs "
"sont là : ##TELEPHONE##\nCordialement,\n##PRENOM## ##NOM## "
", pour la K-Fêt de l'ENS Ulm")
context['mail'] = mail
return context
@teamkfet_required
def order_to_inventory(request, pk):
order = get_object_or_404(Order, pk=pk)
if hasattr(order, 'inventory'):
raise Http404
supplier_prefetch = Prefetch(
'article__supplierarticle_set',
queryset=(
SupplierArticle.objects
.filter(supplier=order.supplier)
.order_by('-at')
),
to_attr='supplier',
)
order_articles = (
OrderArticle.objects
.filter(order=order.pk)
.select_related('article', 'article__category')
.prefetch_related(
supplier_prefetch,
)
.order_by('article__category__name', 'article__name')
)
initial = []
for order_article in order_articles:
article = order_article.article
initial.append({
'article': article.pk,
'name': article.name,
'category': article.category_id,
'category__name': article.category.name,
'quantity_ordered': order_article.quantity_ordered,
'quantity_received': order_article.quantity_ordered,
'price_HT': article.supplier[0].price_HT,
'TVA': article.supplier[0].TVA,
'rights': article.supplier[0].rights,
})
cls_formset = formset_factory(OrderArticleToInventoryForm, extra=0)
if request.method == 'POST':
formset = cls_formset(request.POST, initial=initial)
if not request.user.has_perm('kfet.order_to_inventory'):
messages.error(request, 'Permission refusée')
elif formset.is_valid():
with transaction.atomic():
inventory = Inventory.objects.create(
order=order, by=request.user.profile.account_kfet,
)
new_supplierarticle = []
new_inventoryarticle = []
for form in formset:
q_received = form.cleaned_data['quantity_received']
article = form.cleaned_data['article']
price_HT = form.cleaned_data['price_HT']
TVA = form.cleaned_data['TVA']
rights = form.cleaned_data['rights']
if any((form.initial['price_HT'] != price_HT,
form.initial['TVA'] != TVA,
form.initial['rights'] != rights)):
new_supplierarticle.append(
SupplierArticle(
supplier=order.supplier,
article=article,
price_HT=price_HT,
TVA=TVA,
rights=rights,
)
)
(
OrderArticle.objects
.filter(order=order, article=article)
.update(quantity_received=q_received)
)
new_inventoryarticle.append(
InventoryArticle(
inventory=inventory,
article=article,
stock_old=article.stock,
stock_new=article.stock + q_received,
)
)
article.stock += q_received
if q_received > 0:
article.is_sold = True
article.save()
SupplierArticle.objects.bulk_create(new_supplierarticle)
InventoryArticle.objects.bulk_create(new_inventoryarticle)
messages.success(request, "C'est tout bon !")
return redirect('kfet.order')
else:
messages.error(request, "Corrigez les erreurs")
else:
formset = cls_formset(initial=initial)
return render(request, 'kfet/order_to_inventory.html', {
'formset': formset,
'order': order,
})
class SupplierUpdate(SuccessMessageMixin, UpdateView):
model = Supplier
template_name = 'kfet/supplier_form.html'
fields = ['name', 'address', 'email', 'phone', 'comment']
success_url = reverse_lazy('kfet.order')
sucess_message = 'Données fournisseur mis à jour'
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_supplier'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(SupplierUpdate, self).form_valid(form)
# ==========
# Statistics
# ==========
# ---------------
# Vues génériques
# ---------------
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
class JSONResponseMixin(object):
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(
self.get_data(context),
**response_kwargs
)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
class JSONDetailView(JSONResponseMixin, BaseDetailView):
"""Returns a DetailView that renders a JSON."""
def render_to_response(self, context):
return self.render_to_json_response(context)
class PkUrlMixin(object):
def get_object(self, *args, **kwargs):
get_by = self.kwargs.get(self.pk_url_kwarg)
return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by})
class SingleResumeStat(JSONDetailView):
"""Manifest for a kind of a stat about an object.
Returns JSON whose payload is an array containing descriptions of a stat:
url to retrieve data, label, ...
"""
id_prefix = ''
nb_default = 0
stats = []
url_stat = None
def get_context_data(self, **kwargs):
# On n'hérite pas
object_id = self.object.id
context = {}
stats = []
prefix = '{}_{}'.format(self.id_prefix, object_id)
for i, stat_def in enumerate(self.stats):
url_pk = getattr(self.object, self.pk_url_kwarg)
url_params_d = stat_def.get('url_params', {})
if len(url_params_d) > 0:
url_params = '?{}'.format(urlencode(url_params_d))
else:
url_params = ''
stats.append({
'label': stat_def['label'],
'btn': 'btn_{}_{}'.format(prefix, i),
'url': '{url}{params}'.format(
url=reverse(self.url_stat, args=[url_pk]),
params=url_params,
),
})
context['id_prefix'] = prefix
context['content_id'] = "content_%s" % prefix
context['stats'] = stats
context['default_stat'] = self.nb_default
context['object_id'] = object_id
return context
# -----------------------
# Evolution Balance perso
# -----------------------
ID_PREFIX_ACC_BALANCE = "balance_acc"
class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
"""Manifest for balance stats of an account."""
model = Account
context_object_name = 'account'
pk_url_kwarg = 'trigramme'
url_stat = 'kfet.account.stat.balance'
id_prefix = ID_PREFIX_ACC_BALANCE
stats = [
{
'label': 'Tout le temps',
},
{
'label': '1 an',
'url_params': {'last_days': 365},
},
{
'label': '6 mois',
'url_params': {'last_days': 183},
},
{
'label': '3 mois',
'url_params': {'last_days': 90},
},
{
'label': '30 jours',
'url_params': {'last_days': 30},
},
]
nb_default = 0
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user != obj.user:
raise PermissionDenied
return obj
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
class AccountStatBalance(PkUrlMixin, JSONDetailView):
"""Datasets of balance of an account.
Operations and Transfers are taken into account.
"""
model = Account
pk_url_kwarg = 'trigramme'
context_object_name = 'account'
def get_changes_list(self, last_days=None, begin_date=None, end_date=None):
account = self.object
# prepare filters
if last_days is not None:
end_date = timezone.now()
begin_date = end_date - timezone.timedelta(days=last_days)
# prepare querysets
# TODO: retirer les opgroup dont tous les op sont annulées
opegroups = OperationGroup.objects.filter(on_acc=account)
transfers = (
Transfer.objects
.filter(canceled_at=None)
.select_related('group')
)
recv_transfers = transfers.filter(to_acc=account)
sent_transfers = transfers.filter(from_acc=account)
# apply filters
if begin_date is not None:
opegroups = opegroups.filter(at__gte=begin_date)
recv_transfers = recv_transfers.filter(group__at__gte=begin_date)
sent_transfers = sent_transfers.filter(group__at__gte=begin_date)
if end_date is not None:
opegroups = opegroups.filter(at__lte=end_date)
recv_transfers = recv_transfers.filter(group__at__lte=end_date)
sent_transfers = sent_transfers.filter(group__at__lte=end_date)
# On transforme tout ça en une liste de dictionnaires sous la forme
# {'at': date,
# 'amount': changement de la balance (négatif si diminue la balance,
# positif si l'augmente),
# 'label': text descriptif,
# 'balance': état de la balance après l'action (0 pour le moment,
# sera mis à jour lors d'une
# autre passe)
# }
actions = []
actions.append({
'at': (begin_date or account.created_at).isoformat(),
'amount': 0,
'balance': 0,
})
actions.append({
'at': (end_date or timezone.now()).isoformat(),
'amount': 0,
'balance': 0,
})
actions += [
{
'at': ope_grp.at.isoformat(),
'amount': ope_grp.amount,
'balance': 0,
} for ope_grp in opegroups
] + [
{
'at': tr.group.at.isoformat(),
'amount': tr.amount,
'balance': 0,
} for tr in recv_transfers
] + [
{
'at': tr.group.at.isoformat(),
'amount': -tr.amount,
'balance': 0,
} for tr in sent_transfers
]
# Maintenant on trie la liste des actions par ordre du plus récent
# an plus ancien et on met à jour la balance
if len(actions) > 1:
actions = sorted(actions, key=lambda k: k['at'], reverse=True)
actions[0]['balance'] = account.balance
for i in range(len(actions)-1):
actions[i+1]['balance'] = \
actions[i]['balance'] - actions[i+1]['amount']
return actions
def get_context_data(self, *args, **kwargs):
context = {}
last_days = self.request.GET.get('last_days', None)
if last_days is not None:
last_days = int(last_days)
begin_date = self.request.GET.get('begin_date', None)
end_date = self.request.GET.get('end_date', None)
changes = self.get_changes_list(
last_days=last_days,
begin_date=begin_date, end_date=end_date,
)
context['charts'] = [{
"color": "rgb(200, 20, 60)",
"label": "Balance",
"values": changes,
}]
context['is_time_chart'] = True
if len(changes) > 0:
context['min_date'] = changes[-1]['at']
context['max_date'] = changes[0]['at']
# TODO: offset
return context
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user != obj.user:
raise PermissionDenied
return obj
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
# ------------------------
# Consommation personnelle
# ------------------------
ID_PREFIX_ACC_LAST = "last_acc"
ID_PREFIX_ACC_LAST_DAYS = "last_days_acc"
ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc"
ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
"""Manifest for operations stats of an account."""
model = Account
context_object_name = 'account'
pk_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST
nb_default = 2
stats = last_stats_manifest(types=[Operation.PURCHASE])
url_stat = 'kfet.account.stat.operation'
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user != obj.user:
raise PermissionDenied
return obj
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
"""Datasets of operations of an account."""
model = Account
pk_url_kwarg = 'trigramme'
context_object_name = 'account'
id_prefix = ""
def get_operations(self, scale, types=None):
# On selectionne les opérations qui correspondent
# à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps
all_operations = (
Operation.objects
.filter(group__on_acc=self.object,
canceled_at=None)
.values('article_nb', 'group__at')
.order_by('group__at')
)
if types is not None:
all_operations = all_operations.filter(type__in=types)
chunks = scale.get_by_chunks(
all_operations, field_db='group__at',
field_callback=(lambda d: d['group__at']),
)
return chunks
def get_context_data(self, *args, **kwargs):
old_ctx = super().get_context_data(*args, **kwargs)
context = {'labels': old_ctx['labels']}
scale = self.scale
types = self.request.GET.get('types', None)
if types is not None:
types = ast.literal_eval(types)
operations = self.get_operations(types=types, scale=scale)
# On compte les opérations
nb_ventes = []
for chunk in operations:
ventes = sum(ope['article_nb'] for ope in chunk)
nb_ventes.append(ventes)
context['charts'] = [{"color": "rgb(200, 20, 60)",
"label": "NB items achetés",
"values": nb_ventes}]
return context
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user != obj.user:
raise PermissionDenied
return obj
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
# ------------------------
# Article Satistiques Last
# ------------------------
ID_PREFIX_ART_LAST = "last_art"
ID_PREFIX_ART_LAST_DAYS = "last_days_art"
ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
class ArticleStatSalesList(SingleResumeStat):
"""Manifest for sales stats of an article."""
model = Article
context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST
nb_default = 2
url_stat = 'kfet.article.stat.sales'
stats = last_stats_manifest()
@method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
class ArticleStatSales(ScaleMixin, JSONDetailView):
"""Datasets of sales of an article."""
model = Article
context_object_name = 'article'
def get_context_data(self, *args, **kwargs):
old_ctx = super().get_context_data(*args, **kwargs)
context = {'labels': old_ctx['labels']}
scale = self.scale
all_purchases = (
Operation.objects
.filter(
type=Operation.PURCHASE,
article=self.object,
canceled_at=None,
)
.values('group__at', 'article_nb')
.order_by('group__at')
)
liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ')
liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ')
chunks_liq = scale.get_by_chunks(
liq_only, field_db='group__at',
field_callback=lambda d: d['group__at'],
)
chunks_no_liq = scale.get_by_chunks(
liq_exclude, field_db='group__at',
field_callback=lambda d: d['group__at'],
)
# On compte les opérations
nb_ventes = []
nb_accounts = []
nb_liq = []
for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq):
sum_accounts = sum(ope['article_nb'] for ope in chunk_no_liq)
sum_liq = sum(ope['article_nb'] for ope in chunk_liq)
nb_ventes.append(sum_accounts + sum_liq)
nb_accounts.append(sum_accounts)
nb_liq.append(sum_liq)
context['charts'] = [{"color": "rgb(200, 20, 60)",
"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)