From f414cec6230c2b988a7d8e0169e1d2cd4c917d54 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Fri, 12 Jan 2018 01:07:25 +0100 Subject: [PATCH] Rechargement via comnpay du solde. --- cotisations/forms.py | 8 ++ cotisations/models.py | 2 +- cotisations/payment.py | 101 ++++++++++++++++++ cotisations/payment_utils/__init__.py | 0 cotisations/payment_utils/comnpay.py | 68 ++++++++++++ .../templates/cotisations/payment.html | 37 +++++++ .../templates/cotisations/recharge.html | 39 +++++++ cotisations/urls.py | 16 +++ cotisations/views.py | 23 ++++ machines/models.py | 1 + preferences/forms.py | 2 +- .../migrations/0029_auto_20180111_1134.py | 20 ++++ .../migrations/0030_auto_20180111_2346.py | 24 +++++ preferences/models.py | 4 +- .../preferences/display_preferences.html | 4 +- 15 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 cotisations/payment.py create mode 100644 cotisations/payment_utils/__init__.py create mode 100644 cotisations/payment_utils/comnpay.py create mode 100644 cotisations/templates/cotisations/payment.html create mode 100644 cotisations/templates/cotisations/recharge.html create mode 100644 preferences/migrations/0029_auto_20180111_1134.py create mode 100644 preferences/migrations/0030_auto_20180111_2346.py diff --git a/cotisations/forms.py b/cotisations/forms.py index bbe50b69..303ee32e 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -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 = [] + ) diff --git a/cotisations/models.py b/cotisations/models.py index a775237c..07c3b8fa 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -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\ diff --git a/cotisations/payment.py b/cotisations/payment.py new file mode 100644 index 00000000..ab7260b2 --- /dev/null +++ b/cotisations/payment.py @@ -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 +} diff --git a/cotisations/payment_utils/__init__.py b/cotisations/payment_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cotisations/payment_utils/comnpay.py b/cotisations/payment_utils/comnpay.py new file mode 100644 index 00000000..6c1701d3 --- /dev/null +++ b/cotisations/payment_utils/comnpay.py @@ -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 += '' + + 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 + diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html new file mode 100644 index 00000000..46f26784 --- /dev/null +++ b/cotisations/templates/cotisations/payment.html @@ -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 %} +

Recharger de {{ amount }} €

+
+ {{ content | safe }} + {% bootstrap_button "Payer" button_type="submit" icon="piggy-bank" %} +
+{% endblock %} diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html new file mode 100644 index 00000000..6ca68696 --- /dev/null +++ b/cotisations/templates/cotisations/recharge.html @@ -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 %} +

Rechargement du solde

+

Solde de l'utilisateur : {{ request.user.solde }} €

+
+ {% csrf_token %} + {% bootstrap_form rechargeform %} + {% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %} +
+{% endblock %} diff --git a/cotisations/urls.py b/cotisations/urls.py index cbeeb8eb..0040e48c 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -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[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'), ] diff --git a/cotisations/views.py b/cotisations/views.py index e338943e..10e85ed0 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -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) diff --git a/machines/models.py b/machines/models.py index 59965983..15864707 100644 --- a/machines/models.py +++ b/machines/models.py @@ -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') + diff --git a/preferences/forms.py b/preferences/forms.py index 2f79b6fb..1fa42efc 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -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): diff --git a/preferences/migrations/0029_auto_20180111_1134.py b/preferences/migrations/0029_auto_20180111_1134.py new file mode 100644 index 00000000..92220312 --- /dev/null +++ b/preferences/migrations/0029_auto_20180111_1134.py @@ -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), + ), + ] diff --git a/preferences/migrations/0030_auto_20180111_2346.py b/preferences/migrations/0030_auto_20180111_2346.py new file mode 100644 index 00000000..7f912bbd --- /dev/null +++ b/preferences/migrations/0030_auto_20180111_2346.py @@ -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), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 1d4b9bd2..44bb6580 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -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( diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 38649e16..d483cdb4 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -55,8 +55,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Creations d'users par tous {{ useroptions.all_can_create }} {% if useroptions.user_solde %} - Rechargement max - {{ useroptions.max_recharge }} + Solde maximum + {{ useroptions.max_solde }} {% endif %}