8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-05 09:26:27 +00:00

Users can pay their own cotisation with their solde.

This commit is contained in:
Yoann Pétri 2018-01-11 19:25:41 +01:00 committed by root
parent 891014babd
commit d9ebb266d5
11 changed files with 358 additions and 16 deletions

View file

@ -26,9 +26,8 @@ importé par les views.
Permet de créer une nouvelle facture pour un user (NewFactureForm), Permet de créer une nouvelle facture pour un user (NewFactureForm),
et de l'editer (soit l'user avec EditFactureForm, et de l'editer (soit l'user avec EditFactureForm,
soit le trésorier avec TrezEdit qui a plus de possibilités que self soit le trésorier avec TrezEdit qui a plus de possibilités que self
notamment sur le controle trésorier) notamment sur le controle trésorier SelectArticleForm est utilisée
lors de la creation d'une facture en
SelectArticleForm est utilisée lors de la creation d'une facture en
parrallèle de NewFacture pour le choix des articles désirés. parrallèle de NewFacture pour le choix des articles désirés.
(la vue correspondante est unique) (la vue correspondante est unique)
@ -40,7 +39,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Q from django.db.models import Q
from django.forms import ModelForm, Form from django.forms import ModelForm, Form
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator,MaxValueValidator
from .models import Article, Paiement, Facture, Banque from .models import Article, Paiement, Facture, Banque
from re2o.field_permissions import FieldPermissionFormMixin from re2o.field_permissions import FieldPermissionFormMixin
@ -246,3 +245,37 @@ class DelBanqueForm(Form):
self.fields['banques'].queryset = instances self.fields['banques'].queryset = instances
else: else:
self.fields['banques'].queryset = Banque.objects.all() self.fields['banques'].queryset = Banque.objects.all()
class NewFactureSoldeForm(NewFactureForm):
"""Creation d'une facture, moyen de paiement, banque et numero
de cheque"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['cheque'].required = False
self.fields['banque'].required = False
self.fields['cheque'].label = 'Numero de chèque'
self.fields['banque'].empty_label = "Non renseigné"
self.fields['paiement'].empty_label = "Séléctionner\
une bite de paiement"
paiement_list = Paiement.objects.filter(type_paiement=1)
if paiement_list:
self.fields['paiement'].widget\
.attrs['data-cheque'] = paiement_list.first().id
class Meta:
model = Facture
fields = ['paiement', 'banque']
def clean(self):
cleaned_data = super(NewFactureSoldeForm, self).clean()
paiement = cleaned_data.get("paiement")
cheque = cleaned_data.get("cheque")
banque = cleaned_data.get("banque")
if not paiement:
raise forms.ValidationError("Le moyen de paiement est obligatoire")
elif paiement.type_paiement == "check" and not (cheque and banque):
raise forms.ValidationError("Le numéro de chèque et\
la banque sont obligatoires.")
return cleaned_data

View file

@ -34,6 +34,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h3>Nouvelle facture</h3> <h3>Nouvelle facture</h3>
<p>
Solde de l'utilisateur : {{ user.solde }} €
</p>
{% bootstrap_form factureform %} {% bootstrap_form factureform %}
{{ venteform.management_form }} {{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select --> <!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->

View file

@ -0,0 +1,157 @@
{% extends "cotisations/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load staticfiles%}
{% block title %}Création et modification de factures{% endblock %}
{% block content %}
{% bootstrap_form_errors venteform.management_form %}
<form class="form" method="post">
{% csrf_token %}
<h3>Nouvelle facture</h3>
{{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
<h3>Articles de la facture</h3>
<div id="form_set" class="form-group">
{% for form in venteform.forms %}
<div class='product_to_sell form-inline'>
Article : &nbsp;
{% bootstrap_form form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-0-article-remove" type="button">
<span class="glyphicon glyphicon-remove"></span>
</button>
</div>
{% endfor %}
</div>
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un article" id="add_one">
<p>
Prix total : <span id="total_price">0,00</span>
</p>
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form>
<script type="text/javascript">
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var template = `Article : &nbsp;
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="glyphicon glyphicon-remove"></span>
</button>`
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
'id_form-' + i.toString() + '-article').value;
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity;
}
document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ',');
}
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
}
)
}
function set_cheque_info_visibility() {
var paiement = document.getElementById("id_Facture-paiement");
var visible = paiement.value == paiement.getAttribute('data-cheque');
p = document.getElementById("id_Facture-paiement");
var display = 'none';
if (visible) {
display = 'block';
}
document.getElementById("id_Facture-cheque")
.parentNode.style.display = display;
document.getElementById("id_Facture-banque")
.parentNode.style.display = display;
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
document.getElementById("id_Facture-paiement")
.addEventListener("change", set_cheque_info_visibility, true);
set_cheque_info_visibility();
update_price();
});
</script>
{% endblock %}

View file

@ -26,6 +26,7 @@ from django.conf.urls import url
import re2o import re2o
from . import views from . import views
from . import payment
urlpatterns = [ urlpatterns = [
url(r'^new_facture/(?P<userid>[0-9]+)$', url(r'^new_facture/(?P<userid>[0-9]+)$',
@ -110,5 +111,9 @@ urlpatterns = [
views.control, views.control,
name='control' name='control'
), ),
url(r'^new_facture_solde/(?P<userid>[0-9]+)$',
views.new_facture_solde,
name='new_facture_solde'
),
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
] ]

View file

@ -29,6 +29,7 @@ import os
from django.urls import reverse from django.urls import reverse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.validators import MaxValueValidator
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages from django.contrib import messages
from django.db.models import ProtectedError from django.db.models import ProtectedError
@ -67,7 +68,9 @@ from .forms import (
NewFactureFormPdf, NewFactureFormPdf,
SelectUserArticleForm, SelectUserArticleForm,
SelectClubArticleForm, SelectClubArticleForm,
CreditSoldeForm CreditSoldeForm,
NewFactureSoldeForm,
RechargeForm
) )
from .tex import render_invoice from .tex import render_invoice
@ -584,3 +587,98 @@ def index(request):
return render(request, 'cotisations/index.html', { return render(request, 'cotisations/index.html', {
'facture_list': facture_list 'facture_list': facture_list
}) })
@login_required
def new_facture_solde(request, userid):
"""Creation d'une facture pour un user. Renvoie la liste des articles
et crée des factures dans un formset. Utilise un peu de js coté template
pour ajouter des articles.
Parse les article et boucle dans le formset puis save les ventes,
enfin sauve la facture parente.
TODO : simplifier cette fonction, déplacer l'intelligence coté models
Facture et Vente."""
user = request.user
facture = Facture(user=user)
paiement, _created = Paiement.objects.get_or_create(moyen='Solde')
facture.paiement = paiement
# Le template a besoin de connaitre les articles pour le js
article_list = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
if request.user.is_class_club:
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
else:
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
if article_formset.is_valid():
articles = article_formset
# Si au moins un article est rempli
if any(art.cleaned_data for art in articles):
options, _created = OptionalUser.objects.get_or_create()
user_solde = options.user_solde
solde_negatif = options.solde_negatif
# Si on paye par solde, que l'option est activée,
# on vérifie que le négatif n'est pas atteint
if user_solde:
prix_total = 0
for art_item in articles:
if art_item.cleaned_data:
prix_total += art_item.cleaned_data['article']\
.prix*art_item.cleaned_data['quantity']
if float(user.solde) - float(prix_total) < solde_negatif:
messages.error(request, "Le solde est insuffisant pour\
effectuer l'opération")
return redirect(reverse(
'users:profil',
kwargs={'userid': userid}
))
with transaction.atomic(), reversion.create_revision():
facture.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
for art_item in articles:
if art_item.cleaned_data:
article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity']
new_vente = Vente.objects.create(
facture=facture,
name=article.name,
prix=article.prix,
type_cotisation=article.type_cotisation,
duration=article.duration,
number=quantity
)
with transaction.atomic(), reversion.create_revision():
new_vente.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
if any(art_item.cleaned_data['article'].type_cotisation
for art_item in articles if art_item.cleaned_data):
messages.success(
request,
"La cotisation a été prolongée\
pour l'adhérent %s jusqu'au %s" % (
user.pseudo, user.end_adhesion()
)
)
else:
messages.success(request, "La facture a été crée")
return redirect(reverse(
'users:profil',
kwargs={'userid': userid}
))
messages.error(
request,
u"Il faut au moins un article valide pour créer une facture"
)
return redirect(reverse(
'users:profil',
kwargs={'userid': userid}
))
return form({
'venteform': article_formset,
'articlelist': article_list
}, 'cotisations/new_facture_solde.html', request)

View file

@ -48,6 +48,7 @@ class EditOptionalUserForm(ModelForm):
téléphone' téléphone'
self.fields['user_solde'].label = 'Activation du solde pour\ self.fields['user_solde'].label = 'Activation du solde pour\
les utilisateurs' les utilisateurs'
self.fields['max_recharge'].label = 'Rechargement max'
class EditOptionalMachineForm(ModelForm): class EditOptionalMachineForm(ModelForm):

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-11 10:29
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0027_merge_20180106_2019'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='max_recharge',
field=models.DecimalField(decimal_places=2, default=100, max_digits=5),
),
]

View file

@ -41,6 +41,11 @@ class OptionalUser(models.Model):
decimal_places=2, decimal_places=2,
default=0 default=0
) )
max_recharge = models.DecimalField(
max_digits=5,
decimal_places=2,
default=100
)
gpg_fingerprint = models.BooleanField(default=True) gpg_fingerprint = models.BooleanField(default=True)
all_can_create = models.BooleanField( all_can_create = models.BooleanField(
default=False, default=False,
@ -107,7 +112,10 @@ class OptionalUser(models.Model):
def clean(self): def clean(self):
"""Creation du mode de paiement par solde""" """Creation du mode de paiement par solde"""
if self.user_solde: if self.user_solde:
cotisations.models.Paiement.objects.get_or_create(moyen="Solde") p = cotisations.models.Paiement.objects.filter(moyen="Solde")
if not len(p):
c = cotisations.models.Paiement(moyen="Solde")
c.save()
class OptionalMachine(models.Model): class OptionalMachine(models.Model):
@ -436,7 +444,14 @@ class AssoOption(models.Model):
blank=True, blank=True,
null=True null=True
) )
PAYMENT = (
('NONE', 'NONE'),
('COMNPAY', 'COMNPAY'),
)
payment = models.CharField(max_length=255,
choices=PAYMENT,
default='NONE',
)
class Meta: class Meta:
permissions = ( permissions = (
("view_assooption", "Peut voir les options de l'asso"), ("view_assooption", "Peut voir les options de l'asso"),

View file

@ -54,6 +54,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Creations d'users par tous</th> <th>Creations d'users par tous</th>
<td>{{ useroptions.all_can_create }}</td> <td>{{ useroptions.all_can_create }}</td>
{% if useroptions.user_solde %}
<th>Rechargement max</th>
<td>{{ useroptions.max_recharge }}</td>
{% endif %}
</tr> </tr>
</table> </table>
<h4>Préférences machines</h4> <h4>Préférences machines</h4>
@ -159,7 +163,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Objet utilisateur de l'association</th> <th>Objet utilisateur de l'association</th>
<td>{{ assooptions.utilisateur_asso }}</td> <td>{{ assooptions.utilisateur_asso }}</td>
<th>Moyen de paiement automatique</th>
<td>{{ assooptions.payment }}</td>
</tr> </tr>
</table> </table>
<h4>Messages personalisé dans les mails</h4> <h4>Messages personalisé dans les mails</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">

View file

@ -409,13 +409,11 @@ class User(FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin):
options, _created = OptionalUser.objects.get_or_create() options, _created = OptionalUser.objects.get_or_create()
user_solde = options.user_solde user_solde = options.user_solde
if user_solde: if user_solde:
solde_object, _created = Paiement.objects.get_or_create( solde_objects = Paiement.objects.filter(moyen='Solde')
moyen='Solde'
)
somme_debit = Vente.objects.filter( somme_debit = Vente.objects.filter(
facture__in=Facture.objects.filter( facture__in=Facture.objects.filter(
user=self, user=self,
paiement=solde_object paiement__in=solde_objects
) )
).aggregate( ).aggregate(
total=models.Sum( total=models.Sum(

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}Profil{% endblock %} {% block title %}Profil{% endblock %}
{% block content %} {% block content %}
<h2>{{ users.class_name }}</h2> <h2>{{ users.class_name }} : {{ users.surname }} {{users.name}}</h2>
<div> <div>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="glyphicon glyphicon-edit"></i> <i class="glyphicon glyphicon-edit"></i>
@ -135,13 +135,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if user_solde %} {% if user_solde %}
<tr> <tr>
<th>Solde</th> <th>Solde</th>
<td>{{ users.solde }} €</td> <td>{{ users.solde }} €
</tr> <a class="btn btn-primary btn-sm" style='float:right' role="button" href="{% url 'cotisations:recharge' %}">
<i class="glyphicon glyphicon-piggy-bank"></i>
Recharger
</a>
</td>
{% endif %} {% endif %}
{% if users.shell %} {% if users.shell %}
<th>Shell</th> <th>Shell</th>
<td>{{ users.shell }}</td> <td>{{ users.shell }}</td>
{% endif %} {% endif %}
</tr>
</table> </table>
{% if users.is_class_club %} {% if users.is_class_club %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-club-admin-members' users.club.id %}">
@ -191,7 +196,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<p>Aucune machine</p> <p>Aucune machine</p>
{% endif %} {% endif %}
<h2>Cotisations</h2> <h2>Cotisations</h2>
<h4>{% can_create Facture %}<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}"><i class="glyphicon glyphicon-piggy-bank"></i> Ajouter une cotisation</a>{% acl_end %} {% if user_solde %}<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}"><i class="glyphicon glyphicon-piggy-bank"></i> Modifier le solde</a>{% endif%}</h4> <h4>{% can_create Facture %}<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}"><i class="glyphicon glyphicon-piggy-bank"></i> Ajouter une cotisation</a> {% if user_solde %}<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.id %}"><i class="glyphicon glyphicon-piggy-bank"></i> Modifier le solde</a>{% endif%}{% acl_else %}<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new_facture_solde' user.id %}"><i class="glyphicon glyphicon-piggy-bank"></i> Ajouter une cotisation par solde</a>{% acl_end %}</h4>
{% if facture_list %} {% if facture_list %}
{% include "cotisations/aff_cotisations.html" with facture_list=facture_list %} {% include "cotisations/aff_cotisations.html" with facture_list=facture_list %}
{% else %} {% else %}