mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-25 08:23:46 +00:00
Rechargement via comnpay du solde.
This commit is contained in:
parent
f80d32be98
commit
f414cec623
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\
|
raise forms.ValidationError("Le numéro de chèque et\
|
||||||
la banque sont obligatoires.")
|
la banque sont obligatoires.")
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class RechargeForm(Form):
|
||||||
|
value = forms.FloatField(
|
||||||
|
label='Valeur',
|
||||||
|
min_value=0.01,
|
||||||
|
validators = []
|
||||||
|
)
|
||||||
|
|
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,
|
views.new_facture_solde,
|
||||||
name='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'),
|
url(r'^$', views.index, name='index'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -37,6 +37,8 @@ from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import modelformset_factory, formset_factory
|
from django.forms import modelformset_factory, formset_factory
|
||||||
from django.utils import timezone
|
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 import revisions as reversion
|
||||||
from reversion.models import Version
|
from reversion.models import Version
|
||||||
# Import des models, forms et fonctions re2o
|
# Import des models, forms et fonctions re2o
|
||||||
|
@ -72,6 +74,7 @@ from .forms import (
|
||||||
NewFactureSoldeForm,
|
NewFactureSoldeForm,
|
||||||
RechargeForm
|
RechargeForm
|
||||||
)
|
)
|
||||||
|
from . import payment
|
||||||
from .tex import render_invoice
|
from .tex import render_invoice
|
||||||
|
|
||||||
|
|
||||||
|
@ -682,3 +685,23 @@ def new_facture_solde(request, userid):
|
||||||
}, 'cotisations/new_facture_solde.html', request)
|
}, '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):
|
def text_post_delete(sender, **kwargs):
|
||||||
"""Regeneration dns après modification d'un SRV"""
|
"""Regeneration dns après modification d'un SRV"""
|
||||||
regen('dns')
|
regen('dns')
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ class EditOptionalUserForm(ModelForm):
|
||||||
téléphone'
|
téléphone'
|
||||||
self.fields['user_solde'].label = 'Activation du solde pour\
|
self.fields['user_solde'].label = 'Activation du solde pour\
|
||||||
les utilisateurs'
|
les utilisateurs'
|
||||||
self.fields['max_recharge'].label = 'Rechargement max'
|
self.fields['max_solde'].label = 'Solde maximum'
|
||||||
|
|
||||||
|
|
||||||
class EditOptionalMachineForm(ModelForm):
|
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,
|
decimal_places=2,
|
||||||
default=0
|
default=0
|
||||||
)
|
)
|
||||||
max_recharge = models.DecimalField(
|
max_solde = models.DecimalField(
|
||||||
max_digits=5,
|
max_digits=5,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=100
|
default=50
|
||||||
)
|
)
|
||||||
gpg_fingerprint = models.BooleanField(default=True)
|
gpg_fingerprint = models.BooleanField(default=True)
|
||||||
all_can_create = models.BooleanField(
|
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>
|
<th>Creations d'users par tous</th>
|
||||||
<td>{{ useroptions.all_can_create }}</td>
|
<td>{{ useroptions.all_can_create }}</td>
|
||||||
{% if useroptions.user_solde %}
|
{% if useroptions.user_solde %}
|
||||||
<th>Rechargement max</th>
|
<th>Solde maximum</th>
|
||||||
<td>{{ useroptions.max_recharge }}</td>
|
<td>{{ useroptions.max_solde }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
Loading…
Reference in a new issue