kpsul/kfet/views.py
Aurélien Delobelle 510e16eecf 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

524 lines
19 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.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('account.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('account.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)
@permission_required('kfet.is_team')
def kpsul_perform_operations(request):
# Initializing response data
data = defaultdict(list)
# Checking operationgroup
operationgroup_form = KPsulOperationGroupForm(request.POST)
if not operationgroup_form.is_valid():
data['errors'].append({'operation_group': list(operationgroup_form.errors)})
# Checking operation_formset
operation_formset = KPsulOperationFormSet(request.POST)
if not operation_formset.is_valid():
data['errors'].append({'operations': list(operation_formset.errors) })
# Returning BAD REQUEST if errors
if 'errors' in data:
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 = []
cof_grant_divisor = 1 + cof_grant / 100
is_addcost = (addcost_for and addcost_amount
and addcost_for != operationgroup.on_acc)
addcost_total = 0
# 1. Calculating amount of each PURCHASE operations
# 1.1 Standard price for n articles
# 1.2 Adding addcost if there is one
# 1.3 Taking into account cof status
# 2. Updating (no commit) stock of article for PURCHASE operations
# 3. Calculating amount of operation group
# 4. Adding required permissions to perform each operation
# 5. Updating (no commit) new addcost_for's balance if there is one
# and adding addcost_for in operation instance
for operation in operations:
if operation.type == Operation.PURCHASE:
# 1.1
operation.amount = - operation.article.price * operation.article_nb
if is_addcost:
# 2
operation.addcost_amount = addcost_amount * operation.article_nb
operation.amount -= operation.addcost_amount
# 5
addcost_total += operation.addcost_amount
operation.addcost_for = addcost_for
# 1.3
if operationgroup.on_acc.is_cof:
operation.amount = operation.amount / cof_grant_divisor
# 2
operation.article.stock -= operation.article_nb
# 3
operationgroup.amount += operation.amount
# 4
if operation.type == Operation.DEPOSIT:
required_perms.append('kfet.perform_deposit')
# Starting transaction to ensure data consistency
# Using select_for_update where it is critical
try:
with transaction.atomic():
on_acc = operationgroup.on_acc
on_acc = Account.objects.select_for_update().get(pk=on_acc.pk)
# Adding required permissions to perform operation group
(opegroup_perms, stop_ope) = on_acc.perms_to_perform_operation(
amount = operationgroup.amount)
required_perms += opegroup_perms
# Checking authenticated user has all perms
if stop_ope or not request.user.has_perms(required_perms):
raise PermissionDenied
# If 1 perm is required, saving who perform the operations
if len(required_perms) > 0:
operationgroup.valid_by = request.user.profile.account_kfet
# Filling cof status for statistics
operationgroup.is_cof = on_acc.is_cof
# Saving account's balance and adding to Negative if not in
on_acc.balance += operationgroup.amount
if 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 = on_acc, start = timezone.now())
negative.save()
elif (hasattr(on_acc, 'negative')
and not on_acc.negative.balance_offset):
on_acc.negative.delete()
on_acc.save()
# Saving addcost_for with new balance if there is one
if is_addcost:
addcost_for.balance += addcost_total
addcost_for.save()
# Saving operation group
operationgroup.save()
data['operationgroup'] = operationgroup.pk
# Filling operationgroup id for each operations and saving
# Saving articles with new stock
for operation in operations:
operation.group = operationgroup
operation.save()
if operation.type == Operation.PURCHASE:
operation.article.save()
data['operations'].append(operation.pk)
except PermissionDenied:
# Sending BAD_REQUEST with missing perms or url to manage negative
missing_perms = \
[ Permission.objects.get(codename=codename).name for codename in (
(perm.split('.'))[1] for perm in
required_perms if not request.user.has_perm(perm)
)]
if missing_perms:
data['errors'].append({'missing_perms': missing_perms })
if stop_ope:
data['errors'].append({'negative': 'url to manage negative'})
return JsonResponse(data, status=403)
return JsonResponse(data)