510e16eecf
* 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...
523 lines
19 KiB
Python
523 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)
|