mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-23 07:23:46 +00:00
Merge branch 'master' into ipv6_avance
This commit is contained in:
commit
0389c8ca10
44 changed files with 1144 additions and 46 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ re2o.png
|
|||
__pycache__/*
|
||||
static_files/*
|
||||
static/logo/*
|
||||
media/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -289,7 +289,7 @@ class Vente(models.Model):
|
|||
if not user_request.has_perm('cotisations.change_vente'):
|
||||
return False, u"Vous n'avez pas le droit d'éditer les ventes"
|
||||
elif not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
|
||||
return False, u"Vous ne pouvez pas éditer les factures de cet user protégé"
|
||||
return False, u"Vous ne pouvez pas éditer les factures de cet user protégé"
|
||||
elif not user_request.has_perm('cotisations.change_all_vente') and\
|
||||
(self.facture.control or not self.facture.valid):
|
||||
return False, u"Vous n'avez pas le droit d'éditer une vente\
|
||||
|
|
113
cotisations/payment.py
Normal file
113
cotisations/payment.py
Normal 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
|
||||
}
|
0
cotisations/payment_utils/__init__.py
Normal file
0
cotisations/payment_utils/__init__.py
Normal file
68
cotisations/payment_utils/comnpay.py
Normal file
68
cotisations/payment_utils/comnpay.py
Normal 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
|
||||
|
|
@ -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 -->
|
||||
|
|
157
cotisations/templates/cotisations/new_facture_solde.html
Normal file
157
cotisations/templates/cotisations/new_facture_solde.html
Normal 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 :
|
||||
{% bootstrap_form form label_class='sr-only' %}
|
||||
|
||||
<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 :
|
||||
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
|
||||
|
||||
<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 %}
|
||||
|
37
cotisations/templates/cotisations/payment.html
Normal file
37
cotisations/templates/cotisations/payment.html
Normal 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 %}
|
39
cotisations/templates/cotisations/recharge.html
Normal file
39
cotisations/templates/cotisations/recharge.html
Normal 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 %}
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -107,7 +108,7 @@ clear
|
|||
|
||||
|
||||
if [ $sql_is_local == 2 ]
|
||||
then
|
||||
then
|
||||
TITLE="Login sql"
|
||||
sql_login=$(dialog --title "$TITLE" \
|
||||
--backtitle "$BACKTITLE" \
|
||||
|
@ -169,7 +170,7 @@ ldap_password=$(dialog --title "$TITLE" \
|
|||
2>&1 >/dev/tty)
|
||||
clear
|
||||
if [ $ldap_is_local == 2 ]
|
||||
then
|
||||
then
|
||||
TITLE="Cn ldap admin"
|
||||
ldap_cn=$(dialog --title "$TITLE" \
|
||||
--backtitle "$BACKTITLE" \
|
||||
|
@ -209,7 +210,7 @@ email_port=$(dialog --clear \
|
|||
2>&1 >/dev/tty)
|
||||
clear
|
||||
if [ $ldap_is_local == 2 ]
|
||||
then
|
||||
then
|
||||
TITLE="Cn ldap admin"
|
||||
ldap_cn=$(dialog --title "$TITLE" \
|
||||
--backtitle "$BACKTITLE" \
|
||||
|
@ -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
|
||||
|
@ -253,7 +254,7 @@ then
|
|||
echo $mysql_command
|
||||
while true; do
|
||||
read -p "Continue (y/n)?" choice
|
||||
case "$choice" in
|
||||
case "$choice" in
|
||||
y|Y ) break;;
|
||||
n|N ) exit;;
|
||||
* ) echo "invalid";;
|
||||
|
@ -276,14 +277,14 @@ else
|
|||
echo sudo -u postgres psql $pgsql_command3
|
||||
while true; do
|
||||
read -p "Continue (y/n)?" choice
|
||||
case "$choice" in
|
||||
case "$choice" in
|
||||
y|Y ) break;;
|
||||
n|N ) exit;;
|
||||
* ) echo "invalid";;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $ldap_is_local == 1 ]
|
||||
then
|
||||
|
@ -430,7 +431,7 @@ if [ ! -z "$1" ]
|
|||
then
|
||||
if [ $1 == ldap ]
|
||||
then
|
||||
if [ ! -z "$2" ]
|
||||
if [ ! -z "$2" ]
|
||||
then
|
||||
echo Installation du ldap
|
||||
setup_ldap $2 $3
|
||||
|
|
|
@ -2271,3 +2271,4 @@ def srv_post_save(sender, **kwargs):
|
|||
def text_post_delete(sender, **kwargs):
|
||||
"""Regeneration dns après modification d'un SRV"""
|
||||
regen('dns')
|
||||
|
||||
|
|
59
preferences/aes_field.py
Normal file
59
preferences/aes_field.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
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):
|
||||
if value is None:
|
||||
return value
|
||||
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):
|
||||
if value is None:
|
||||
return value
|
||||
return binascii.b2a_base64(encrypt(
|
||||
settings.AES_KEY,
|
||||
value
|
||||
))
|
|
@ -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):
|
||||
|
|
20
preferences/migrations/0028_auto_20180111_1129.py
Normal file
20
preferences/migrations/0028_auto_20180111_1129.py
Normal 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),
|
||||
),
|
||||
]
|
20
preferences/migrations/0029_auto_20180111_1134.py
Normal file
20
preferences/migrations/0029_auto_20180111_1134.py
Normal 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),
|
||||
),
|
||||
]
|
24
preferences/migrations/0030_auto_20180111_2346.py
Normal file
24
preferences/migrations/0030_auto_20180111_2346.py
Normal 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),
|
||||
),
|
||||
]
|
20
preferences/migrations/0031_optionaluser_self_adhesion.py
Normal file
20
preferences/migrations/0031_optionaluser_self_adhesion.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
20
preferences/migrations/0033_generaloption_gtu_sum_up.py
Normal file
20
preferences/migrations/0033_generaloption_gtu_sum_up.py
Normal 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'),
|
||||
),
|
||||
]
|
25
preferences/migrations/0034_auto_20180114_2025.py
Normal file
25
preferences/migrations/0034_auto_20180114_2025.py
Normal 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=''),
|
||||
),
|
||||
]
|
20
preferences/migrations/0035_auto_20180114_2132.py
Normal file
20
preferences/migrations/0035_auto_20180114_2132.py
Normal 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/'),
|
||||
),
|
||||
]
|
20
preferences/migrations/0036_auto_20180114_2141.py
Normal file
20
preferences/migrations/0036_auto_20180114_2141.py
Normal 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=''),
|
||||
),
|
||||
]
|
20
preferences/migrations/0037_auto_20180114_2156.py
Normal file
20
preferences/migrations/0037_auto_20180114_2156.py
Normal 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=''),
|
||||
),
|
||||
]
|
20
preferences/migrations/0038_auto_20180114_2209.py
Normal file
20
preferences/migrations/0038_auto_20180114_2209.py
Normal 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=''),
|
||||
),
|
||||
]
|
21
preferences/migrations/0039_auto_20180115_0003.py
Normal file
21
preferences/migrations/0039_auto_20180115_0003.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- 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),
|
||||
),
|
||||
]
|
26
preferences/migrations/0040_auto_20180129_1745.py
Normal file
26
preferences/migrations/0040_auto_20180129_1745.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2018-01-29 16:45
|
||||
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.AddField(
|
||||
model_name='assooption',
|
||||
name='payment_pass',
|
||||
field=preferences.aes_field.AESEncryptedField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assooption',
|
||||
name='payment_id',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
),
|
||||
]
|
|
@ -29,6 +29,8 @@ from django.utils.functional import cached_property
|
|||
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,
|
||||
|
@ -42,11 +44,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 = (
|
||||
|
@ -108,7 +124,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):
|
||||
|
@ -303,6 +322,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 = (
|
||||
|
@ -454,6 +483,24 @@ 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,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
|
|
|
@ -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' %}">
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -150,7 +150,7 @@ STATICFILES_DIRS = (
|
|||
),
|
||||
)
|
||||
|
||||
MEDIA_ROOT = '/var/www/re2o/static'
|
||||
MEDIA_ROOT = '/var/www/re2o/media'
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
30
re2o/templatetags/self_adhesion.py
Normal file
30
re2o/templatetags/self_adhesion.py
Normal 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
|
|
@ -1,3 +1,4 @@
|
|||
django-bootstrap3
|
||||
django-macaddress
|
||||
python-dateutil
|
||||
pycrypto
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -153,7 +153,7 @@ class UserManager(BaseUserManager):
|
|||
user.set_password(password)
|
||||
if su:
|
||||
user.is_superuser=True
|
||||
user.save(using=self._db)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_user(self, pseudo, surname, email, password=None):
|
||||
|
@ -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."
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue