8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-06-18 08:38:09 +00:00

POC des moyens de paiements sous forme de modules.

This commit is contained in:
Hugo LEVY-FALK 2018-06-21 20:03:46 +02:00
parent d37364ee8f
commit e0d71ed291
20 changed files with 417 additions and 68 deletions

View file

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 17:28
from __future__ import unicode_literals
import cotisations.payment_methods.comnpay.aes_field
from django.db import migrations, models
import django.db.models.deletion
def add_cheque(apps, schema_editor):
ChequePayment = apps.get_model('cotisations', 'ChequePayment')
Payment = apps.get_model('cotisations', 'Paiement')
for p in Payment.objects.filter(type_paiement=1):
cheque = ChequePayment()
cheque.payment = p
cheque.save()
def add_comnpay(apps, schema_editor):
ComnpayPayment = apps.get_model('cotisations', 'ComnpayPayment')
Payment = apps.get_model('cotisations', 'Paiement')
AssoOption = apps.get_model('preferences', 'AssoOption')
options, _created = AssoOption.objects.get_or_create()
payment, _created = Payment.objects.get_or_create(
moyen='Rechargement en ligne'
)
comnpay = ComnpayPayment()
comnpay.payment_user = options.payment_id
comnpay.payment_pass = options.payment_pass
comnpay.payment = payment
comnpay.save()
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0031_article_allow_self_subscription'),
]
operations = [
migrations.CreateModel(
name='ChequePayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
),
migrations.CreateModel(
name='ComnpayPayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_user', models.CharField(blank=True, default='', max_length=255)),
('payment_pass', cotisations.payment_methods.comnpay.aes_field.AESEncryptedField(blank=True, max_length=255, null=True)),
('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
),
migrations.RunPython(add_cheque),
migrations.RunPython(add_comnpay),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-28 19:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0032_chequepayment_comnpaypayment'),
]
operations = [
migrations.AlterField(
model_name='comnpaypayment',
name='payment_id',
field=models.CharField(blank=True, default='', max_length=255),
),
]

View file

@ -42,6 +42,9 @@ from django.core.validators import MinValueValidator
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _l
from django.urls import reverse
from django.shortcuts import redirect
from django.contrib import messages
from machines.models import regen
from re2o.field_permissions import FieldPermissionModelMixin
@ -629,6 +632,36 @@ class Paiement(RevMixin, AclMixin, models.Model):
)
super(Paiement, self).save(*args, **kwargs)
def end_payment(self, invoice, request):
"""
The general way of ending a payment. You may redefine this method for custom
payment methods. Must return a HttpResponse-like object.
"""
if hasattr(self, 'payment_method'):
return self.payment_method.end_payment(invoice, request)
# In case a cotisation was bought, inform the user, the
# cotisation time has been extended too
if any(sell.type_cotisation for sell in invoice.vente_set.all()):
messages.success(
request,
_("The cotisation of %(member_name)s has been \
extended to %(end_date)s.") % {
'member_name': request.user.pseudo,
'end_date': request.user.end_adhesion()
}
)
# Else, only tell the invoice was created
else:
messages.success(
request,
_("The invoice has been created.")
)
return redirect(reverse(
'users:profil',
kwargs={'userid': request.user.pk}
))
class Cotisation(RevMixin, AclMixin, models.Model):
"""

View file

@ -0,0 +1,9 @@
from django.conf.urls import include, url
from . import comnpay, cheque
urlpatterns = [
url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')),
url(r'^cheque/', include(cheque.urls, namespace='cheque')),
]

View file

@ -0,0 +1,7 @@
"""
This module contains a method to pay online using cheque.
"""
from . import models, urls, views
NAME = "CHEQUE"
Payment = models.ChequePayment

View file

@ -0,0 +1,10 @@
from django import forms
from django.utils.translation import ugettext_lazy as _l
from cotisations.models import Banque as Bank
class ChequeForm(forms.Form):
"""A simple form to get the bank a the cheque number."""
bank = forms.ModelChoiceField(Bank.objects.all(), label=_l("Bank"))
number = forms.CharField(label=_l("Cheque number"))

View file

@ -0,0 +1,21 @@
from django.db import models
from django.shortcuts import redirect
from django.urls import reverse
from cotisations.models import Paiement as BasePayment
class ChequePayment(models.Model):
"""
The model allowing you to pay with a cheque. It redefines post_payment
method. See `cotisations.models.Paiement for further details.
"""
payment = models.OneToOneField(BasePayment, related_name='payment_method')
def end_payment(self, invoice, request):
invoice.valid = False
invoice.save()
return redirect(reverse(
'cotisations:cheque:validate',
kwargs={'invoice_pk': invoice.pk}
))

View file

@ -0,0 +1,10 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(
r'^validate/(?P<invoice_pk>[0-9]+)$',
views.cheque,
name='validate'
)
]

View file

@ -0,0 +1,45 @@
"""Payment
Here are defined some views dedicated to cheque payement.
"""
from django.urls import reverse
from django.shortcuts import redirect, render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils.translation import ugettext as _
from cotisations.models import Facture as Invoice
from .models import ChequePayment
from .forms import ChequeForm
@login_required
def cheque(request, invoice_pk):
invoice = get_object_or_404(Invoice, pk=invoice_pk)
payment_method = getattr(invoice.paiement, 'payment_method', None)
if invoice.valid or not isinstance(payment_method, ChequePayment):
messages.error(
request,
_("You cannot pay this invoice with a cheque.")
)
return redirect(reverse(
'users:profil',
kwargs={'userid': request.user.pk}
))
form = ChequeForm(request.POST or None)
if form.is_valid():
invoice.banque = form.cleaned_data['bank']
invoice.cheque = form.cleaned_data['number']
invoice.valid = True
invoice.save()
return redirect(reverse(
'users:profil',
kwargs={'userid': request.user.pk}
))
return render(
request,
'cotisations/payment_form.html',
{'form': form}
)

View file

@ -0,0 +1,6 @@
"""
This module contains a method to pay online using comnpay.
"""
from . import models, urls, views
NAME = "COMNPAY"
Payment = models.ComnpayPayment

View file

@ -0,0 +1,94 @@
# coding:utf-8
# 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
# Copyright © 2018 Maël Kervella
#
# 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.
# App de gestion des machines pour re2o
# Gabriel Détraz, Augustin Lemesle
# Gplv2
"""preferences.aes_field
Module defining a AESEncryptedField object that can be used in forms
to handle the use of properly encrypting and decrypting AES keys
"""
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):
""" Generate a random string of length `length` and composed of
the characters in `chars` """
return ''.join([choice(chars) for i in range(length)])
def encrypt(key, s):
""" AES Encrypt a secret `s` with the key `key` """
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):
""" AES Decrypt a secret `s` with the key `key` """
obj = AES.new(key)
ss = obj.decrypt(s)
return ss.split(bytes(EOD, 'utf-8'))[0]
class AESEncryptedField(models.CharField):
""" A Field that can be used in forms for adding the support
of AES ecnrypted fields """
def save_form_data(self, instance, data):
setattr(instance, self.name,
binascii.b2a_base64(encrypt(settings.AES_KEY, data)))
def to_python(self, value):
if value is None:
return None
return decrypt(settings.AES_KEY,
binascii.a2b_base64(value)).decode('utf-8')
def from_db_value(self, value, *args, **kwargs):
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
))

View file

@ -10,7 +10,7 @@ import hashlib
from collections import OrderedDict
class Payment():
class Transaction():
""" The class representing a transaction with all the functions
used during the negociation
"""

View file

@ -0,0 +1,30 @@
from django.db import models
from django.shortcuts import render
from cotisations.models import Paiement as BasePayment
from .aes_field import AESEncryptedField
from .views import comnpay
class ComnpayPayment(models.Model):
"""
The model allowing you to pay with COMNPAY. It redefines post_payment
method. See `cotisations.models.Paiement for further details.
"""
payment = models.OneToOneField(BasePayment, related_name='payment_method')
payment_credential = models.CharField(
max_length=255,
default='',
blank=True
)
payment_pass = AESEncryptedField(
max_length=255,
null=True,
blank=True,
)
def end_payment(self, invoice, request):
content = comnpay(invoice, request)
return render(request, 'cotisations/payment.html', content)

View file

@ -0,0 +1,20 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(
r'^accept/(?P<factureid>[0-9]+)$',
views.accept_payment,
name='accept_payment'
),
url(
r'^refuse/$',
views.refuse_payment,
name='refuse_payment'
),
url(
r'^ipn/$',
views.ipn,
name='ipn'
),
]

View file

@ -15,8 +15,8 @@ from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpResponseBadRequest
from preferences.models import AssoOption
from .models import Facture
from .payment_utils.comnpay import Payment as ComnpayPayment
from cotisations.models import Facture
from .comnpay import Transaction
@csrf_exempt
@ -73,7 +73,7 @@ def ipn(request):
Verify that we can firmly save the user's action and notify
Comnpay with 400 response if not or with a 200 response if yes
"""
p = ComnpayPayment()
p = Transaction()
order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
try:
data = OrderedDict([(f, request.POST[f]) for f in order])
@ -121,15 +121,15 @@ def comnpay(facture, request):
the preferences.
"""
host = request.get_host()
p = ComnpayPayment(
p = Transaction(
str(AssoOption.get_cached_value('payment_id')),
str(AssoOption.get_cached_value('payment_pass')),
'https://' + host + reverse(
'cotisations:accept_payment',
'cotisations:comnpay_accept_payment',
kwargs={'factureid': facture.id}
),
'https://' + host + reverse('cotisations:refuse_payment'),
'https://' + host + reverse('cotisations:ipn'),
'https://' + host + reverse('cotisations:comnpay_refuse_payment'),
'https://' + host + reverse('cotisations:comnpay_ipn'),
"",
"D"
)
@ -145,9 +145,3 @@ def comnpay(facture, request):
}
return r
# The payment systems supported by re2o
PAYMENT_SYSTEM = {
'COMNPAY': comnpay,
'NONE': None
}

View file

@ -0,0 +1,37 @@
{% extends "cotisations/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2018 Hugo Levy-Falk
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%}
{% load i18n %}
{% block title %}{% trans form.title %}{% endblock %}
{% block content %}
<h2>{% trans form.title %}</h2>
<form class="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %}
</form>
{% endblock %}

View file

@ -29,7 +29,7 @@ from django.conf.urls import url
import re2o
from . import views
from . import payment
from . import payment_methods
urlpatterns = [
url(
@ -143,20 +143,6 @@ urlpatterns = [
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'),
]
] + payment_methods.urlpatterns

View file

@ -73,7 +73,6 @@ from .forms import (
CreditSoldeForm,
RechargeForm
)
from . import payment as online_payment
from .tex import render_invoice
@ -159,11 +158,6 @@ def new_facture(request, user, userid):
'users:profil',
kwargs={'userid': userid}
))
is_online_payment = new_invoice_instance.paiement == (
Paiement.objects.get_or_create(
moyen='Rechargement en ligne')[0])
new_invoice_instance.valid = not is_online_payment
# Saving the invoice
new_invoice_instance.save()
@ -182,34 +176,8 @@ def new_facture(request, user, userid):
)
new_purchase.save()
if is_online_payment:
content = online_payment.PAYMENT_SYSTEM[
AssoOption.get_cached_value('payment')
](new_invoice_instance, request)
return render(request, 'cotisations/payment.html', content)
return new_invoice_instance.paiement.end_payment(new_invoice_instance, request)
# In case a cotisation was bought, inform the user, the
# cotisation time has been extended too
if any(art_item.cleaned_data['article'].type_cotisation
for art_item in articles if art_item.cleaned_data):
messages.success(
request,
_("The cotisation of %(member_name)s has been \
extended to %(end_date)s.") % {
'member_name': user.pseudo,
'end_date': user.end_adhesion()
}
)
# Else, only tell the invoice was created
else:
messages.success(
request,
_("The invoice has been created.")
)
return redirect(reverse(
'users:profil',
kwargs={'userid': userid}
))
messages.error(
request,
_("You need to choose at least one article.")
@ -894,9 +862,9 @@ def recharge(request):
number=1
)
purchase.save()
content = online_payment.PAYMENT_SYSTEM[
AssoOption.get_cached_value('payment')
](invoice, request)
# content = online_payment.PAYMENT_SYSTEM[
# AssoOption.get_cached_value('payment')
# ](invoice, request)
return render(request, 'cotisations/payment.html', content)
return form({
'rechargeform': refill_form,

View file

@ -897,7 +897,7 @@ def profil(request, users, **_kwargs):
SortTable.USERS_INDEX_WHITE
)
user_solde = OptionalUser.get_cached_value('user_solde')
allow_online_payment = AssoOption.get_cached_value('payment') != 'NONE'
allow_online_payment = True# TODO : AssoOption.get_cached_value('payment') != 'NONE'
return render(
request,
'users/profil.html',