mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-08-20 14:13:41 +00:00
Rechargement via comnpay du solde.
This commit is contained in:
parent
d9ebb266d5
commit
f7657a2236
15 changed files with 343 additions and 6 deletions
|
@ -279,3 +279,11 @@ class NewFactureSoldeForm(NewFactureForm):
|
|||
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 = []
|
||||
)
|
||||
|
|
|
@ -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\
|
||||
|
|
101
cotisations/payment.py
Normal file
101
cotisations/payment.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
"""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 .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):
|
||||
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, "DEMO"):
|
||||
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 == "DEMO":
|
||||
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, host):
|
||||
p = ComnpayPayment(
|
||||
"DEMO",
|
||||
"DEMO",
|
||||
'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
|
||||
|
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 %}
|
||||
<h3>Rechargement du solde</h3>
|
||||
<p>Solde de l'utilisateur : {{ request.user.solde }} €</p>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form rechargeform %}
|
||||
{% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -115,5 +115,21 @@ urlpatterns = [
|
|||
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'),
|
||||
]
|
||||
|
|
|
@ -37,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
|
||||
|
@ -72,6 +74,7 @@ from .forms import (
|
|||
NewFactureSoldeForm,
|
||||
RechargeForm
|
||||
)
|
||||
from . import payment
|
||||
from .tex import render_invoice
|
||||
|
||||
|
||||
|
@ -682,3 +685,23 @@ def new_facture_solde(request, userid):
|
|||
}, 'cotisations/new_facture_solde.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
def recharge(request):
|
||||
f = RechargeForm(request.POST or None)
|
||||
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()
|
||||
options, _created = AssoOption.objects.get_or_create()
|
||||
content = payment.PAYMENT_SYSTEM[options.payment](facture, request.get_host())
|
||||
return render(request, 'cotisations/payment.html', content)
|
||||
return form({'rechargeform':f}, 'cotisations/recharge.html', request)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ class EditOptionalUserForm(ModelForm):
|
|||
téléphone'
|
||||
self.fields['user_solde'].label = 'Activation du solde pour\
|
||||
les utilisateurs'
|
||||
self.fields['max_recharge'].label = 'Rechargement max'
|
||||
self.fields['max_solde'].label = 'Solde maximum'
|
||||
|
||||
|
||||
class EditOptionalMachineForm(ModelForm):
|
||||
|
|
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),
|
||||
),
|
||||
]
|
|
@ -41,10 +41,10 @@ class OptionalUser(models.Model):
|
|||
decimal_places=2,
|
||||
default=0
|
||||
)
|
||||
max_recharge = models.DecimalField(
|
||||
max_solde = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
default=100
|
||||
default=50
|
||||
)
|
||||
gpg_fingerprint = models.BooleanField(default=True)
|
||||
all_can_create = models.BooleanField(
|
||||
|
|
|
@ -55,8 +55,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<th>Creations d'users par tous</th>
|
||||
<td>{{ useroptions.all_can_create }}</td>
|
||||
{% if useroptions.user_solde %}
|
||||
<th>Rechargement max</th>
|
||||
<td>{{ useroptions.max_recharge }}</td>
|
||||
<th>Solde maximum</th>
|
||||
<td>{{ useroptions.max_solde }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
|
|
Loading…
Reference in a new issue