8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-25 22:03:10 +00:00

Merge branch 'online_payement' into 'master'

Online payement

See merge request federez/re2o!68
This commit is contained in:
chirac 2018-01-28 16:51:06 +01:00
commit 3dd87c9446
44 changed files with 1144 additions and 46 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ re2o.png
__pycache__/*
static_files/*
static/logo/*
media/*

View file

@ -26,9 +26,8 @@ importé par les views.
Permet de créer une nouvelle facture pour un user (NewFactureForm),
et de l'editer (soit l'user avec EditFactureForm,
soit le trésorier avec TrezEdit qui a plus de possibilités que self
notamment sur le controle trésorier)
SelectArticleForm est utilisée lors de la creation d'une facture en
notamment sur le controle trésorier SelectArticleForm est utilisée
lors de la creation d'une facture en
parrallèle de NewFacture pour le choix des articles désirés.
(la vue correspondante est unique)
@ -40,8 +39,10 @@ from __future__ import unicode_literals
from django import forms
from django.db.models import Q
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 preferences.models import OptionalUser
from users.models import User
from re2o.field_permissions import FieldPermissionFormMixin
@ -246,3 +247,58 @@ class DelBanqueForm(Form):
self.fields['banques'].queryset = instances
else:
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
class RechargeForm(Form):
value = forms.FloatField(
label='Valeur',
min_value=0.01,
validators = []
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(RechargeForm, self).__init__(*args, **kwargs)
def clean_value(self):
value = self.cleaned_data['value']
options, _created = OptionalUser.objects.get_or_create()
if value < options.min_online_payment:
raise forms.ValidationError("Montant inférieur au montant minimal de paiement en ligne (%s) €" % options.min_online_payment)
if value + self.user.solde > options.max_solde:
raise forms.ValidationError("Le solde ne peux excéder %s " % options.max_solde)
return value

113
cotisations/payment.py Normal file
View file

@ -0,0 +1,113 @@
"""Payment
Here are defined some views dedicated to online payement.
"""
from django.urls import reverse
from django.shortcuts import redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt
from django.utils.datastructures import MultiValueDictKeyError
from django.http import HttpResponse, HttpResponseBadRequest
from collections import OrderedDict
from preferences.models import AssoOption
from .models import Facture
from .payment_utils.comnpay import Payment as ComnpayPayment
@csrf_exempt
@login_required
def accept_payment(request, factureid):
facture = get_object_or_404(Facture, id=factureid)
messages.success(
request,
"Le paiement de {} € a été accepté.".format(facture.prix())
)
return redirect(reverse('users:profil', kwargs={'userid':request.user.id}))
@csrf_exempt
@login_required
def refuse_payment(request):
messages.error(
request,
"Le paiement a été refusé."
)
return redirect(reverse('users:profil', kwargs={'userid':request.user.id}))
@csrf_exempt
def ipn(request):
option, _created = AssoOption.objects.get_or_create()
p = ComnpayPayment()
order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
try:
data = OrderedDict([(f, request.POST[f]) for f in order])
except MultiValueDictKeyError:
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
if not p.validSec(data, option.payment_pass):
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
result = True if (request.POST['result'] == 'OK') else False
idTpe = request.POST['idTpe']
idTransaction = request.POST['idTransaction']
# On vérifie que le paiement nous est destiné
if not idTpe == option.payment_id:
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
try:
factureid = int(idTransaction)
except ValueError:
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
facture = get_object_or_404(Facture, id=factureid)
# On vérifie que le paiement est valide
if not result:
# Le paiement a échoué : on effectue les actions nécessaires (On indique qu'elle a échoué)
facture.delete()
# On notifie au serveur ComNPay qu'on a reçu les données pour traitement
return HttpResponse("HTTP/1.1 200 OK")
facture.valid = True
facture.save()
# A nouveau, on notifie au serveur qu'on a bien traité les données
return HttpResponse("HTTP/1.0 200 OK")
def comnpay(facture, request):
host = request.get_host()
option, _created = AssoOption.objects.get_or_create()
p = ComnpayPayment(
str(option.payment_id),
str(option.payment_pass),
'https://' + host + reverse(
'cotisations:accept_payment',
kwargs={'factureid':facture.id}
),
'https://' + host + reverse('cotisations:refuse_payment'),
'https://' + host + reverse('cotisations:ipn'),
"",
"D"
)
r = {
'action' : 'https://secure.homologation.comnpay.com',
'method' : 'POST',
'content' : p.buildSecretHTML(
"Rechargement du solde",
facture.prix(),
idTransaction=str(facture.id)
),
'amount' : facture.prix,
}
return r
PAYMENT_SYSTEM = {
'COMNPAY' : comnpay,
'NONE' : None
}

View file

View file

@ -0,0 +1,68 @@
import time
from random import randrange
import base64
import hashlib
from collections import OrderedDict
from itertools import chain
class Payment():
vad_number = ""
secret_key = ""
urlRetourOK = ""
urlRetourNOK = ""
urlIPN = ""
source = ""
typeTr = "D"
def __init__(self, vad_number = "", secret_key = "", urlRetourOK = "", urlRetourNOK = "", urlIPN = "", source="", typeTr="D"):
self.vad_number = vad_number
self.secret_key = secret_key
self.urlRetourOK = urlRetourOK
self.urlRetourNOK = urlRetourNOK
self.urlIPN = urlIPN
self.source = source
self.typeTr = typeTr
def buildSecretHTML(self, produit="Produit", montant="0.00", idTransaction=""):
if idTransaction == "":
self.idTransaction = str(time.time())+self.vad_number+str(randrange(999))
else:
self.idTransaction = idTransaction
array_tpe = OrderedDict(
montant= str(montant),
idTPE= self.vad_number,
idTransaction= self.idTransaction,
devise= "EUR",
lang= 'fr',
nom_produit= produit,
source= self.source,
urlRetourOK= self.urlRetourOK,
urlRetourNOK= self.urlRetourNOK,
typeTr= str(self.typeTr)
)
if self.urlIPN!="":
array_tpe['urlIPN'] = self.urlIPN
array_tpe['key'] = self.secret_key;
strWithKey = base64.b64encode(bytes('|'.join(array_tpe.values()), 'utf-8'))
del array_tpe["key"]
array_tpe['sec'] = hashlib.sha512(strWithKey).hexdigest()
ret = ""
for key in array_tpe:
ret += '<input type="hidden" name="'+key+'" value="'+array_tpe[key]+'"/>'
return ret
def validSec(self, values, secret_key):
if "sec" in values:
sec = values['sec']
del values["sec"]
strWithKey = hashlib.sha512(base64.b64encode(bytes('|'.join(values.values()) +"|"+secret_key, 'utf-8'))).hexdigest()
return strWithKey.upper() == sec.upper()
else:
return False

View file

@ -34,6 +34,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post">
{% csrf_token %}
<h3>Nouvelle facture</h3>
<p>
Solde de l'utilisateur : {{ user.solde }} €
</p>
{% bootstrap_form factureform %}
{{ venteform.management_form }}
<!-- 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

@ -0,0 +1,37 @@
{% 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 %}Rechargement du solde{% endblock %}
{% block content %}
<h3>Recharger de {{ amount }} €</h3>
<form class="form" method="{{ method }}" action="{{action}}">
{{ content | safe }}
{% bootstrap_button "Payer" button_type="submit" icon="piggy-bank" %}
</form>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% 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 %}Rechargement du solde{% endblock %}
{% block content %}
<h2>Rechargement du solde</h2>
<h3>Solde : <span class="label label-default">{{ request.user.solde }} €</span></h3>
<form class="form" method="post">
{% csrf_token %}
{% bootstrap_form rechargeform %}
{% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %}
</form>
{% endblock %}

View file

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

View file

@ -29,6 +29,7 @@ import os
from django.urls import reverse
from django.shortcuts import render, redirect
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 import messages
from django.db.models import ProtectedError
@ -36,6 +37,8 @@ from django.db import transaction
from django.db.models import Q
from django.forms import modelformset_factory, formset_factory
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.debug import sensitive_variables
from reversion import revisions as reversion
from reversion.models import Version
# Import des models, forms et fonctions re2o
@ -67,8 +70,11 @@ from .forms import (
NewFactureFormPdf,
SelectUserArticleForm,
SelectClubArticleForm,
CreditSoldeForm
CreditSoldeForm,
NewFactureSoldeForm,
RechargeForm
)
from . import payment
from .tex import render_invoice
@ -584,3 +590,127 @@ def index(request):
return render(request, 'cotisations/index.html', {
'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)
@login_required
def recharge(request):
options, _created = AssoOption.objects.get_or_create()
if options.payment == 'NONE':
messages.error(
request,
"Le paiement en ligne est désactivé."
)
return redirect(reverse(
'users:profil',
kwargs={'userid': request.user.id}
))
f = RechargeForm(request.POST or None, user=request.user)
if f.is_valid():
facture = Facture(user=request.user)
paiement, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne')
facture.paiement = paiement
facture.valid = False
facture.save()
v = Vente.objects.create(
facture=facture,
name='solde',
prix=f.cleaned_data['value'],
number=1,
)
v.save()
content = payment.PAYMENT_SYSTEM[options.payment](facture, request)
return render(request, 'cotisations/payment.html', content)
return form({'rechargeform':f}, 'cotisations/recharge.html', request)

View file

@ -28,8 +28,9 @@ setup_ldap() {
install_re2o_server() {
echo "Installation de Re2o !
Cet utilitaire va procéder à l'installation initiale de re2o. Le serveur présent doit être vierge.
Preconfiguration..."
export DEBIAN_FRONTEND=noninteractive
@ -236,7 +237,7 @@ install_base=$(dialog --clear \
$HEIGHT $WIDTH \
2>&1 >/dev/tty)
apt-get -y install python3-django python3-dateutil texlive-latex-base texlive-fonts-recommended python3-djangorestframework python3-django-reversion python3-pip libsasl2-dev libldap2-dev libssl-dev
apt-get -y install python3-django python3-dateutil texlive-latex-base texlive-fonts-recommended python3-djangorestframework python3-django-reversion python3-pip libsasl2-dev libldap2-dev libssl-dev python3-crypto
pip3 install django-bootstrap3
pip3 install django-ldapdb
pip3 install django-macaddress

View file

@ -2153,3 +2153,4 @@ def srv_post_save(sender, **kwargs):
def text_post_delete(sender, **kwargs):
"""Regeneration dns après modification d'un SRV"""
regen('dns')

55
preferences/aes_field.py Normal file
View file

@ -0,0 +1,55 @@
import string
import binascii
from random import choice
from Crypto.Cipher import AES
from django.db import models
from django.conf import settings
EOD = '`%EofD%`' # This should be something that will not occur in strings
def genstring(length=16, chars=string.printable):
return ''.join([choice(chars) for i in range(length)])
def encrypt(key, s):
obj = AES.new(key)
datalength = len(s) + len(EOD)
if datalength < 16:
saltlength = 16 - datalength
else:
saltlength = 16 - datalength % 16
ss = ''.join([s, EOD, genstring(saltlength)])
return obj.encrypt(ss)
def decrypt(key, s):
obj = AES.new(key)
ss = obj.decrypt(s)
print(ss)
return ss.split(bytes(EOD, 'utf-8'))[0]
class AESEncryptedField(models.CharField):
def save_form_data(self, instance, data):
setattr(instance, self.name,
binascii.b2a_base64(encrypt(settings.AES_KEY, data)))
def to_python(self, value):
if value is None:
return None
return decrypt(settings.AES_KEY,
binascii.a2b_base64(value)).decode('utf-8')
def from_db_value(self, value, expression, connection, *args):
if value is None:
return value
return decrypt(settings.AES_KEY,
binascii.a2b_base64(value)).decode('utf-8')
def get_prep_value(self, value):
return binascii.b2a_base64(encrypt(
settings.AES_KEY,
value
))

View file

@ -48,6 +48,9 @@ class EditOptionalUserForm(ModelForm):
téléphone'
self.fields['user_solde'].label = 'Activation du solde pour\
les utilisateurs'
self.fields['max_solde'].label = 'Solde maximum'
self.fields['min_online_payment'].label = 'Montant de rechargement minimum en ligne'
self.fields['self_adhesion'].label = 'Auto inscription'
class EditOptionalMachineForm(ModelForm):
@ -114,6 +117,7 @@ class EditGeneralOptionForm(ModelForm):
self.fields['site_name'].label = 'Nom du site web'
self.fields['email_from'].label = "Adresse mail d\
'expedition automatique"
self.fields['GTU_sum_up'].label = "Résumé des CGU"
class EditAssoOptionForm(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

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-11 10:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0028_auto_20180111_1129'),
]
operations = [
migrations.AddField(
model_name='assooption',
name='payment',
field=models.CharField(choices=[('NONE', 'NONE'), ('COMNPAY', 'COMNPAY')], default='NONE', max_length=255),
),
]

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-11 22:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0029_auto_20180111_1134'),
]
operations = [
migrations.RemoveField(
model_name='optionaluser',
name='max_recharge',
),
migrations.AddField(
model_name='optionaluser',
name='max_solde',
field=models.DecimalField(decimal_places=2, default=50, max_digits=5),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-12 11:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0030_auto_20180111_2346'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='self_adhesion',
field=models.BooleanField(default=False, help_text='Un nouvel utilisateur peut se créer son compte sur re2o'),
),
]

View file

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

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 19:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0032_optionaluser_min_online_payment'),
]
operations = [
migrations.AddField(
model_name='generaloption',
name='GTU_sum_up',
field=models.TextField(blank=True, default='', help_text='Résumé des CGU'),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 19:25
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0033_generaloption_gtu_sum_up'),
]
operations = [
migrations.AddField(
model_name='generaloption',
name='GTU',
field=models.FileField(default='', upload_to='GTU'),
),
migrations.AlterField(
model_name='generaloption',
name='GTU_sum_up',
field=models.TextField(blank=True, default=''),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 20:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0034_auto_20180114_2025'),
]
operations = [
migrations.AlterField(
model_name='generaloption',
name='GTU',
field=models.FileField(default='', upload_to='/var/www/static/'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 20:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0035_auto_20180114_2132'),
]
operations = [
migrations.AlterField(
model_name='generaloption',
name='GTU',
field=models.FileField(default='', upload_to=''),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 20:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0036_auto_20180114_2141'),
]
operations = [
migrations.AlterField(
model_name='generaloption',
name='GTU',
field=models.FileField(default='', null=True, upload_to=''),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 21:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0037_auto_20180114_2156'),
]
operations = [
migrations.AlterField(
model_name='generaloption',
name='GTU',
field=models.FileField(blank=True, default='', null=True, upload_to=''),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 23:03
from __future__ import unicode_literals
from django.db import migrations, models
import preferences.aes_field
class Migration(migrations.Migration):
dependencies = [
('preferences', '0038_auto_20180114_2209'),
]
operations = [
migrations.AddField(
model_name='assooption',
name='payment_id',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='assooption',
name='payment_pass',
field=preferences.aes_field.AESEncryptedField(max_length=255, null=True),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-01-14 23:10
from __future__ import unicode_literals
from django.db import migrations, models
import preferences.aes_field
class Migration(migrations.Migration):
dependencies = [
('preferences', '0039_auto_20180115_0003'),
]
operations = [
migrations.AlterField(
model_name='assooption',
name='payment_id',
field=models.CharField(default='', max_length=255),
),
migrations.AlterField(
model_name='assooption',
name='payment_pass',
field=preferences.aes_field.AESEncryptedField(default='', max_length=255),
),
]

View file

@ -28,6 +28,8 @@ from __future__ import unicode_literals
from django.db import models
import cotisations.models
from .aes_field import AESEncryptedField
class OptionalUser(models.Model):
"""Options pour l'user : obligation ou nom du telephone,
@ -41,11 +43,25 @@ class OptionalUser(models.Model):
decimal_places=2,
default=0
)
max_solde = models.DecimalField(
max_digits=5,
decimal_places=2,
default=50
)
min_online_payment = models.DecimalField(
max_digits=5,
decimal_places=2,
default=10
)
gpg_fingerprint = models.BooleanField(default=True)
all_can_create = models.BooleanField(
default=False,
help_text="Tous les users peuvent en créer d'autres",
)
self_adhesion = models.BooleanField(
default=False,
help_text="Un nouvel utilisateur peut se créer son compte sur re2o"
)
class Meta:
permissions = (
@ -107,7 +123,10 @@ class OptionalUser(models.Model):
def clean(self):
"""Creation du mode de paiement par 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):
@ -285,6 +304,16 @@ class GeneralOption(models.Model):
req_expire_hrs = models.IntegerField(default=48)
site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@serveur.net")
GTU_sum_up = models.TextField(
default="",
blank=True,
)
GTU = models.FileField(
upload_to = '',
default="",
null=True,
blank=True,
)
class Meta:
permissions = (
@ -436,6 +465,23 @@ class AssoOption(models.Model):
blank=True,
null=True
)
PAYMENT = (
('NONE', 'NONE'),
('COMNPAY', 'COMNPAY'),
)
payment = models.CharField(max_length=255,
choices=PAYMENT,
default='NONE',
)
payment_id = models.CharField(
max_length=255,
default='',
)
payment_pass = AESEncryptedField(
max_length=255,
default='',
)
class Meta:
permissions = (

View file

@ -54,7 +54,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr>
<th>Creations d'users par tous</th>
<td>{{ useroptions.all_can_create }}</td>
<th>Auto inscription</th>
<td>{{ useroptions.self_adhesion }}</td>
</tr>
{% if useroptions.user_solde %}
<tr>
<th>Solde maximum</th>
<td>{{ useroptions.max_solde }}</td>
<th>Montant minimal de rechargement en ligne</th>
<td>{{ useroptions.min_online_payment }}</td>
</tr>
{% endif %}
</table>
<h4>Préférences machines</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
@ -127,7 +137,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr>
<th>Message global affiché sur le site</th>
<td>{{ generaloptions.general_message }}</td>
<th>Résumé des CGU</th>
<td>{{ generaloptions.GTU_sum_up }}</td>
<tr>
<tr>
<th>CGU</th>
<td>{{generaloptions.GTU}}</th>
</tr>
</table>
<h4>Données de l'association</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
@ -159,7 +175,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr>
<th>Objet utilisateur de l'association</th>
<td>{{ assooptions.utilisateur_asso }}</td>
<th>Moyen de paiement automatique</th>
<td>{{ assooptions.payment }}</td>
</tr>
</table>
<h4>Messages personalisé dans les mails</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">

View file

@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h3>Edition des préférences</h3>
<form class="form" method="post">
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% massive_bootstrap_form options 'utilisateur_asso' %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}

View file

@ -95,6 +95,7 @@ def edit_options(request, section):
return redirect(reverse('index'))
options = form_instance(
request.POST or None,
request.FILES or None,
instance=options_instance
)
if options.is_valid():

View file

@ -45,11 +45,10 @@ def can_create(model):
def decorator(view):
def wrapper(request, *args, **kwargs):
can, msg = model.can_create(request.user, *args, **kwargs)
#options, _created = OptionalUser.objects.get_or_create()
if not can:
messages.error(request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
kwargs={'userid':str(request.user.id)}
))
return redirect(reverse('index'))
return view(request, *args, **kwargs)
return wrapper
return decorator

View file

@ -150,7 +150,7 @@ STATICFILES_DIRS = (
),
)
MEDIA_ROOT = '/var/www/re2o/static'
MEDIA_ROOT = '/var/www/re2o/media'
STATIC_URL = '/static/'

View file

@ -26,6 +26,10 @@ SECRET_KEY = 'SUPER_SECRET_KEY'
DB_PASSWORD = 'SUPER_SECRET_DB'
# AES key for secret key encryption
AES_KEY = 'WHAT_A_WONDERFULL_KEY'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

View file

@ -0,0 +1,30 @@
# 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.
from django import template
from preferences.models import OptionalUser, GeneralOption
register = template.Library()
@register.simple_tag
def self_adhesion():
options, _created = OptionalUser.objects.get_or_create()
return options.self_adhesion

View file

@ -1,3 +1,4 @@
django-bootstrap3
django-macaddress
python-dateutil
pycrypto

View file

@ -26,6 +26,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{# Load the tag library #}
{% load bootstrap3 %}
{% load acl %}
{% load self_adhesion %}
{% self_adhesion as var_sa %}
<!DOCTYPE html>
<html lang="fr">
<head prefix="og: http://ogp.me/ns#">
@ -102,17 +104,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</form>
</div>
<ul class="nav navbar-nav navbar-right">
<li>
{% if request.user.is_authenticated %}
<li>
<a href="{% url 'logout' %}">
<span class="glyphicon glyphicon-log-out"></span> Logout
</a>
</li>
{% else %}
{% if var_sa %}
<li>
<a href="{% url 'users:new-user' %}">
<span class="glyphicon glyphicon-user"></span> Créer un compte
</a>
</li>
{% endif %}
<li>
<a href="{% url 'login' %}">
<span class="glyphicon glyphicon-log-in"></span> Login
</a>
</li>
{% endif %}
</li>
</ul>
{% can_view_app preferences %}
<ul class="nav navbar-nav navbar-right">

View file

@ -409,13 +409,12 @@ class User(FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin):
options, _created = OptionalUser.objects.get_or_create()
user_solde = options.user_solde
if user_solde:
solde_object, _created = Paiement.objects.get_or_create(
moyen='Solde'
)
solde_objects = Paiement.objects.filter(moyen='Solde')
somme_debit = Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self,
paiement=solde_object
paiement__in=solde_objects,
valid=True
)
).aggregate(
total=models.Sum(
@ -424,7 +423,7 @@ class User(FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin):
)
)['total'] or 0
somme_credit = Vente.objects.filter(
facture__in=Facture.objects.filter(user=self),
facture__in=Facture.objects.filter(user=self, valid=True),
name="solde"
).aggregate(
total=models.Sum(
@ -685,10 +684,13 @@ class User(FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin):
an user or if the `options.all_can_create` is set.
"""
options, _created = OptionalUser.objects.get_or_create()
if options.all_can_create:
return True, None
if(not user_request.is_authenticated and not options.self_adhesion):
return False, None
else:
return user_request.has_perm('users.add_user'), u"Vous n'avez pas le\
if(options.all_can_create or options.self_adhesion):
return True, None
else:
return user_request.has_perm('users.add_user'), u"Vous n'avez pas le\
droit de créer un utilisateur"
def can_edit(self, user_request, *args, **kwargs):
@ -863,7 +865,7 @@ class Club(User):
"""
if user_request.has_perm('users.view_user'):
return True, None
if user_request.is_class_adherent:
if hasattr(user_request,'is_class_adherent') and user_request.is_class_adherent:
if user_request.adherent.club_administrator.all() or user_request.adherent.club_members.all():
return True, None
return False, u"Vous n'avez pas accès à la liste des utilisateurs."

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}Profil{% endblock %}
{% block content %}
<h2>{{ users.class_name }}</h2>
<h2>{{ users.class_name }} : {{ users.surname }} {{users.name}}</h2>
<div>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' users.id %}">
<i class="glyphicon glyphicon-edit"></i>
@ -132,16 +132,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>Aucun</td>
{% endif %}
</tr>
{% if user_solde %}
{% if allow_online_payment %}
<tr>
<th>Solde</th>
<td>{{ users.solde }} €</td>
</tr>
<td>{{ users.solde }} €
<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 %}
{% if users.shell %}
<th>Shell</th>
<td>{{ users.shell }}</td>
{% endif %}
</tr>
</table>
{% if users.is_class_club %}
<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>
{% endif %}
<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 %}{% if user_solde %}<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>{% endif %}{% acl_end %}</h4>
{% if facture_list %}
{% include "cotisations/aff_cotisations.html" with facture_list=facture_list %}
{% else %}

View file

@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load acl %}
{% block sidebar %}
{% if request.user.is_authenticated%}
{% can_create Club %}
<a class="list-group-item list-group-item-success" href="{% url "users:new-club" %}">
<i class="glyphicon glyphicon-plus"></i>
@ -37,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Créer un adhérent
</a>
{% acl_end %}
{% endif %}
{% can_view_all Club %}
<a class="list-group-item list-group-item-info" href="{% url "users:index-clubs" %}">
<i class="glyphicon glyphicon-list"></i>

View file

@ -36,6 +36,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% massive_bootstrap_form userform 'room,school,administrators,members' %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form>
<br>
{% if showCGU %}
<p>En cliquant sur Créer ou modifier, l'utilisateur s'engage à respecter les <a href="/media/{{ GTU }}" download="CGU" >règles d'utilisation du réseau</a>.</p>
<h3>Résumé des règles d'utilisations</h3>
<p>{{ GTU_sum_up }}</p>
{% endif %}
<br/>
<br/>
<br/>

View file

@ -85,7 +85,7 @@ from users.forms import (
)
from cotisations.models import Facture
from machines.models import Machine
from preferences.models import OptionalUser, GeneralOption
from preferences.models import OptionalUser, GeneralOption, AssoOption
from re2o.views import form
from re2o.utils import (
@ -117,12 +117,14 @@ def password_change_action(u_form, user, request, req=False):
kwargs={'userid':str(user.id)}
))
@login_required
@can_create(Adherent)
def new_user(request):
""" Vue de création d'un nouvel utilisateur,
envoie un mail pour le mot de passe"""
user = AdherentForm(request.POST or None, user=request.user)
options, _created = GeneralOption.objects.get_or_create()
GTU_sum_up = options.GTU_sum_up
GTU = options.GTU
if user.is_valid():
user = user.save(commit=False)
with transaction.atomic(), reversion.create_revision():
@ -136,7 +138,7 @@ def new_user(request):
'users:profil',
kwargs={'userid':str(user.id)}
))
return form({'userform': user}, 'users/user.html', request)
return form({'userform': user,'GTU_sum_up':GTU_sum_up,'GTU':GTU,'showCGU':True}, 'users/user.html', request)
@login_required
@ -158,7 +160,7 @@ def new_club(request):
'users:profil',
kwargs={'userid':str(club.id)}
))
return form({'userform': club}, 'users/user.html', request)
return form({'userform': club, 'showCGU':False}, 'users/user.html', request)
@login_required
@ -179,7 +181,7 @@ def edit_club_admin_members(request, club_instance, clubid):
'users:profil',
kwargs={'userid':str(club_instance.id)}
))
return form({'userform': club}, 'users/user.html', request)
return form({'userform': club, 'showCGU':False}, 'users/user.html', request)
@login_required
@ -364,7 +366,7 @@ def add_ban(request, user, userid):
request,
"Attention, cet utilisateur a deja un bannissement actif"
)
return form({'userform': ban}, 'users/user.html', request)
return form({'userform': ban}, 'users/user.html', request)
@login_required
@can_edit(Ban)
@ -413,7 +415,7 @@ def add_whitelist(request, user, userid):
request,
"Attention, cet utilisateur a deja un accès gracieux actif"
)
return form({'userform': whitelist}, 'users/user.html', request)
return form({'userform': whitelist}, 'users/user.html', request)
@login_required
@ -780,6 +782,8 @@ def profil(request, users, userid):
)
options, _created = OptionalUser.objects.get_or_create()
user_solde = options.user_solde
options, _created = AssoOption.objects.get_or_create()
allow_online_payment = options.payment != 'NONE'
return render(
request,
'users/profil.html',
@ -790,6 +794,7 @@ def profil(request, users, userid):
'ban_list': bans,
'white_list': whitelists,
'user_solde': user_solde,
'allow_online_payment' : allow_online_payment,
}
)