kpsul/kfet/views.py
Aurélien Delobelle 51c1767ffe Rework validation et enregistrement opés K-Psul
- Utilisations d'update pour les balances et autres afin d'éviter les
  lost update
- Validation tout en un puis enregistrement tout en un
- Ajout du lien vers K-Psul dans le nav
2016-08-11 06:13:31 +02:00

628 lines
24 KiB
Python

from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied, ValidationError
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.core.urlresolvers import 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 HttpResponse, JsonResponse, Http404
from django.forms import modelformset_factory
from django.db import IntegrityError, transaction
from django.db.models import F
from django.utils import timezone
from gestioncof.models import CofProfile, Clipper
from kfet.models import Account, Checkout, Article, Settings, AccountNegative
from kfet.forms import *
from collections import defaultdict
@login_required
def home(request):
return render(request, "kfet/base.html")
def put_cleaned_data_in_dict(dict, form):
for field in form.cleaned_data:
dict[field] = form.cleaned_data[field]
# -----
# Account views
# -----
# Account - General
@login_required
@permission_required('kfet.is_team')
def account(request):
accounts = Account.objects.order_by('trigramme')
return render(request, "kfet/account.html", { 'accounts' : accounts })
@login_required
@permission_required('kfet.is_team')
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
@permission_required('kfet.is_team')
def account_create(request):
# A envoyer au template
data_template = {
'account_trigramme_form': AccountTriForm(),
'errors' : {},
}
# Enregistrement
if request.method == "POST":
# Pour indiquer la tentative d'enregistrement au template
# Checking permission
if not request.user.has_perm('kfet.add_account'):
raise PermissionDenied
# Peuplement des forms
username = request.POST.get('username')
try:
user = User.objects.get(username=username)
(cof, _) = CofProfile.objects.get_or_create(user=user)
user_form = UserForm(request.POST, instance=user)
cof_form = CofForm(request.POST, instance=cof)
except User.DoesNotExist:
user_form = UserForm(request.POST)
cof_form = CofForm(request.POST)
trigramme_form = AccountTriForm(request.POST)
account_form = AccountNoTriForm(request.POST)
# Ajout des erreurs pour le template
data_template['errors']['user_form'] = user_form.errors
data_template['errors']['cof_form'] = cof_form.errors
data_template['errors']['trigramme_form'] = trigramme_form.errors
data_template['errors']['account_form'] = account_form.errors
if all((user_form.is_valid(), cof_form.is_valid(),
trigramme_form.is_valid(), account_form.is_valid())):
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)
except Account.UserHasAccount as e:
messages.error(request, \
"Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme)
return render(request, "kfet/account_create.html", data_template)
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
@login_required
@permission_required('kfet.is_team')
def account_create_ajax(request, username=None, login_clipper=None):
user = None
if login_clipper:
# à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore existé
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)
# Ici, on nous a menti, le user existe déjà
username = user.username
login_clipper = None
except User.DoesNotExist:
# Clipper (sans user déjà existant)
# UserForm - Prefill + Création
user_initial_data = {
'username' : login_clipper,
'email' : login_clipper + "@clipper.ens.fr"}
if clipper.fullname:
# Prefill du nom et prénom
names = clipper.fullname.split()
# Le premier, c'est le prénom
user_initial_data['first_name'] = names[0]
if len(names) > 1:
# Si d'autres noms -> tous dans le nom de famille
user_initial_data['last_name'] = " ".join(names[1:])
user_form = UserForm(initial = user_initial_data)
# CofForm - Prefill + Création
cof_initial_data = { 'login_clipper': login_clipper }
cof_form = CofForm(initial = cof_initial_data)
# AccountForm
account_form = AccountForm()
# Protection (read-only) des champs username et login_clipper
account_form_set_readonly_fields(user_form, cof_form)
if username:
# le user existe déjà
user = get_object_or_404(User, username=username)
# récupération du profil cof
(cof, _) = CofProfile.objects.get_or_create(user=user)
# UserForm + CofForm - Création à partir des instances existantes
user_form = UserForm(instance = user)
cof_form = CofForm(instance = cof)
# AccountForm
account_form = AccountNoTriForm()
# Protection (read-only) des champs username et login_clipper
account_form_set_readonly_fields(user_form, cof_form)
elif not login_clipper:
# connaît pas du tout, faut tout remplir
user_form = UserForm()
cof_form = CofForm()
account_form = AccountNoTriForm()
return render(request, "kfet/account_create_form.html", {
'account_form' : account_form,
'cof_form' : cof_form,
'user_form' : user_form,
})
# Account - Read
@login_required
def account_read(request, trigramme):
try:
account = Account.objects.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
return render(request, "kfet/account_read.html", { 'account' : account })
# Account - Update
@login_required
def account_update(request, trigramme):
try:
account = Account.objects.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
if request.method == "POST":
# Update attempt
# Checking permissions
if not request.user.has_perm('kfet.change_account') \
and request.user != account.user:
raise PermissionDenied
# Peuplement des forms
if request.user.has_perm('kfet.change_account'):
account_form = AccountForm(request.POST, instance = account)
else:
account_form = AccountRestrictForm(request.POST, instance = account)
cof_form = CofRestrictForm(request.POST, instance=account.cofprofile)
user_form = UserRestrictForm(request.POST, instance=account.user)
if all((account_form.is_valid(), cof_form.is_valid(), user_form.is_valid())):
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)
if request.user == account.user:
messages.success(request, \
'Vos informations ont été mises à jour')
else:
messages.success(request, \
'Informations du compte %s mises à jour' % account.trigramme)
return redirect('kfet.account.read', account.trigramme)
else:
messages.error(request, \
'Informations non mises à jour. Corrigez les erreurs')
else:
# No update attempt
if request.user.has_perm('kfet.is_team'):
account_form = AccountForm(instance = account)
else:
account_form = AccountRestrictForm(instance = account)
cof_form = CofRestrictForm(instance = account.cofprofile)
user_form = UserRestrictForm(instance = account.user)
return render(request, "kfet/account_update.html", {
'account' : account,
'account_form' : account_form,
'cof_form' : cof_form,
'user_form' : user_form,
})
# -----
# 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('add_checkout'):
raise PermissionDenied
# Creating
form.instance.created_by = self.request.user.profile.account_kfet
return super(CheckoutCreate, self).form_valid(form)
# Checkout - Read
class CheckoutRead(DetailView):
model = Checkout
template_name = 'kfet/checkout_read.html'
context_object_name = 'checkout'
# 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('change_checkout'):
raise PermissionDenied
# Updating
return super(CheckoutUpdate, self).form_valid(form)
# -----
# Article views
# -----
# Article - General
class ArticleList(ListView):
model = Article
queryset = Article.objects.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('add_article'):
raise PermissionDenied
# 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'
# Article - Update
class ArticleUpdate(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('change_article'):
raise PermissionDenied
# Updating
return super(ArticleUpdate, self).form_valid(form)
# -----
# K-Psul
# -----
@permission_required('kfet.is_team')
def kpsul(request):
data = {}
data['operationgroup_form'] = KPsulOperationGroupForm()
data['trigramme_form'] = KPsulAccountForm()
data['checkout_form'] = KPsulCheckoutForm()
operation_formset = KPsulOperationFormSet(queryset=Operation.objects.none())
data['operation_formset'] = operation_formset
return render(request, 'kfet/kpsul.html', data)
@permission_required('kfet.is_team')
def kpsul_account_data(request):
trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme)
data = { 'pk': 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 }
return JsonResponse(data)
@permission_required('kfet.is_team')
def kpsul_checkout_data(request):
pk = request.POST.get('pk', 0)
checkout = get_object_or_404(Checkout, pk=pk)
data = { 'pk': checkout.pk, 'name': checkout.name, 'balance': checkout.balance,
'valid_from': checkout.valid_from, 'valid_to': checkout.valid_to }
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
@permission_required('kfet.is_team')
def kpsul_perform_operations(request):
# Initializing response data
data = { 'operation_group': 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)
# 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_amounty
to_addcost_for_balance += operation.addcost_amount
if operationgroup.on_acc.is_cash:
to_checkout_balance += -operation.amount
if operationgroup.on_acc.is_cof:
operation.amount = operation.amount / cof_grant_divisor
to_articles_stocks[operation.article] -= operation.article_nb
else:
if operationgroup.on_acc.is_cash:
data['errors']['account'] = 'Charge et retrait impossible sur LIQ'
to_checkout_balance += operation.amount
operationgroup.amount += operation.amount
if operation.type == Operation.DEPOSIT:
required_perms.add('kfet.perform_deposit')
(perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
amount = operationgroup.amount)
required_perms |= perms
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'] = True
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(on_acc, 'negative'):
if not on_acc.negative.start:
on_acc.negative.start = timezone.now()
on_acc.negative.save()
else:
negative = AccountNegative(
account = operationgroup.on_acc, start = timezone.now())
negative.save()
elif (hasattr(on_acc, 'negative')
and not on_acc.negative.balance_offset):
on_acc.negative.delete()
# Updating checkout's balance
if to_checkout_balance:
Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
balance = F('balance') + to_checkout_balance)
# Saving addcost_for with new balance if there is one
if is_addcost and to_addcost_for_balance:
Account.objects.filter(pk=addcost_for.pk).update(
balance = F('balance') + to_addcost_for_balance)
# Saving operation group
operationgroup.save()
data['operationgroup'] = operationgroup.pk
# Filling operationgroup id for each operations and saving
for operation in operations:
operation.group = operationgroup
operation.save()
data['operations'].append(operation.pk)
# Updating articles stock
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
stock = F('stock') + to_articles_stocks[article])
return JsonResponse(data)
@permission_required('kfet.is_team')
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('operation', []))))
except ValueError:
return JsonResponse(data, status=400)
opes_all = (
Operation.objects
.select_related('group', 'group__on_acc', 'group__on_acc__negative')
.filter(pk__in=opes_post))
opes_pk = [ ope.pk for ope in opes_all ]
opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ]
if opes_notexisting:
data['errors']['opes_notexisting'] = opes_notexisting
return JsonResponse(data, status=400)
opes_already_canceled = [] # Déjà annulée
opes = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = Settings.CANCEL_DURATION()
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles
for ope in opes_all:
if ope.canceled_at:
# Opération déjà annulée, va pour un warning en Response
opes_already_canceled.append(ope.pk)
else:
opes.append(ope.pk)
# Si opé il y a plus de CANCEL_DURATION, permission requise
if ope.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
if not ope.group.on_acc.is_cash:
to_accounts_balances[ope.group.on_acc] -= ope.amount
if ope.addcost_for and ope.addcost_amount:
to_accounts_balances[ope.addcost_for] -= ope.addcost_amount
# Pour les groupes d'opés
to_groups_amounts[ope.group] -= ope.amount
# Pour les balances de caisses
if ope.type == Operation.PURCHASE:
if ope.group.on_acc.is_cash:
to_checkouts_balances[ope.group.on_acc] -= - ope.amount
else:
to_checkouts_balances[ope.group.on_acc] -= ope.amount
# Pour les stocks d'articles
if ope.article and ope.article_nb:
to_articles_stocks[ope.article] += ope.article_nb
if not opes:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)
# Checking permissions or stop
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account],
overdraft_duration_max = overdraft_duration_max,
overdraft_amount_max = overdraft_amount_max)
required_perms |= perms
stop_all = stop_all or stop
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'] = True
return JsonResponse(data, status=403)
with transaction.atomic():
canceled_by = required_perms and request.user.profile.account_kfet or None
(Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=timezone.now()))
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])
data['canceled'] = opes
if opes_already_canceled:
data['warnings']['already_canceled'] = opes_already_canceled
return JsonResponse(data)