From d9ebb266d5675094a9528c3b644ebb3b77bb4fc4 Mon Sep 17 00:00:00 2001 From: Yoann PIETRI Date: Thu, 11 Jan 2018 19:25:41 +0100 Subject: [PATCH 01/25] Users can pay their own cotisation with their solde. --- cotisations/forms.py | 41 ++++- .../templates/cotisations/new_facture.html | 3 + .../cotisations/new_facture_solde.html | 157 ++++++++++++++++++ cotisations/urls.py | 5 + cotisations/views.py | 100 ++++++++++- preferences/forms.py | 1 + .../migrations/0028_auto_20180111_1129.py | 20 +++ preferences/models.py | 19 ++- .../preferences/display_preferences.html | 7 + users/models.py | 8 +- users/templates/users/profil.html | 13 +- 11 files changed, 358 insertions(+), 16 deletions(-) create mode 100644 cotisations/templates/cotisations/new_facture_solde.html create mode 100644 preferences/migrations/0028_auto_20180111_1129.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 5845611a..bbe50b69 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -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,7 +39,7 @@ 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 re2o.field_permissions import FieldPermissionFormMixin @@ -246,3 +245,37 @@ 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 diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index f2586e8b..9d466cee 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -34,6 +34,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Nouvelle facture

+

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

{% bootstrap_form factureform %} {{ venteform.management_form }} diff --git a/cotisations/templates/cotisations/new_facture_solde.html b/cotisations/templates/cotisations/new_facture_solde.html new file mode 100644 index 00000000..2efd8e81 --- /dev/null +++ b/cotisations/templates/cotisations/new_facture_solde.html @@ -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 %} + + + {% csrf_token %} +

Nouvelle facture

+ {{ venteform.management_form }} + +

Articles de la facture

+
+ {% for form in venteform.forms %} +
+ Article :   + {% bootstrap_form form label_class='sr-only' %} +   + +
+ {% endfor %} +
+ +

+ Prix total : 0,00 € +

+ {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} +
+ + + +{% endblock %} + diff --git a/cotisations/urls.py b/cotisations/urls.py index 2a0c0163..cbeeb8eb 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -26,6 +26,7 @@ from django.conf.urls import url import re2o from . import views +from . import payment urlpatterns = [ url(r'^new_facture/(?P[0-9]+)$', @@ -110,5 +111,9 @@ urlpatterns = [ views.control, name='control' ), + url(r'^new_facture_solde/(?P[0-9]+)$', + views.new_facture_solde, + name='new_facture_solde' + ), url(r'^$', views.index, name='index'), ] diff --git a/cotisations/views.py b/cotisations/views.py index d7d953c9..e338943e 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -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 @@ -67,7 +68,9 @@ from .forms import ( NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - CreditSoldeForm + CreditSoldeForm, + NewFactureSoldeForm, + RechargeForm ) from .tex import render_invoice @@ -584,3 +587,98 @@ 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) + + diff --git a/preferences/forms.py b/preferences/forms.py index 7dda8620..2f79b6fb 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -48,6 +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' class EditOptionalMachineForm(ModelForm): diff --git a/preferences/migrations/0028_auto_20180111_1129.py b/preferences/migrations/0028_auto_20180111_1129.py new file mode 100644 index 00000000..c6c03719 --- /dev/null +++ b/preferences/migrations/0028_auto_20180111_1129.py @@ -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), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 8dfc4260..1d4b9bd2 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -41,6 +41,11 @@ class OptionalUser(models.Model): decimal_places=2, default=0 ) + max_recharge = models.DecimalField( + max_digits=5, + decimal_places=2, + default=100 + ) gpg_fingerprint = models.BooleanField(default=True) all_can_create = models.BooleanField( default=False, @@ -107,7 +112,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): @@ -436,7 +444,14 @@ class AssoOption(models.Model): blank=True, null=True ) - + PAYMENT = ( + ('NONE', 'NONE'), + ('COMNPAY', 'COMNPAY'), + ) + payment = models.CharField(max_length=255, + choices=PAYMENT, + default='NONE', + ) class Meta: permissions = ( ("view_assooption", "Peut voir les options de l'asso"), diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 7802929d..38649e16 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -54,6 +54,10 @@ 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 }} + {% endif %}

Préférences machines

@@ -159,7 +163,10 @@ with this program; if not, write to the Free Software Foundation, Inc., Objet utilisateur de l'association {{ assooptions.utilisateur_asso }} + Moyen de paiement automatique + {{ assooptions.payment }} +

Messages personalisé dans les mails

diff --git a/users/models.py b/users/models.py index 998678cd..1852c687 100644 --- a/users/models.py +++ b/users/models.py @@ -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,11 @@ 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 ) ).aggregate( total=models.Sum( diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 0e5f30e4..8f399043 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}Profil{% endblock %} {% block content %} -

{{ users.class_name }}

+

{{ users.class_name }} : {{ users.surname }} {{users.name}}

@@ -135,13 +135,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if user_solde %} Solde - {{ users.solde }} € - + {{ users.solde }} € + + + Recharger + + {% endif %} {% if users.shell %} Shell {{ users.shell }} {% endif %} + {% if users.is_class_club %} @@ -191,7 +196,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Aucune machine

{% endif %}

Cotisations

-

{% can_create Facture %} Ajouter une cotisation{% acl_end %} {% if user_solde %} Modifier le solde{% endif%}

+

{% can_create Facture %} Ajouter une cotisation {% if user_solde %} Modifier le solde{% endif%}{% acl_else %} Ajouter une cotisation par solde{% acl_end %}

{% if facture_list %} {% include "cotisations/aff_cotisations.html" with facture_list=facture_list %} {% else %} From f7657a223627490c5465588535fe38d5b96d9b28 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Fri, 12 Jan 2018 01:07:25 +0100 Subject: [PATCH 02/25] 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 %} From cbe541d382758162da57ef009d8fc42434cca968 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Fri, 12 Jan 2018 16:03:48 +0100 Subject: [PATCH 03/25] =?UTF-8?q?Fix,=20le=20solde=20est=20calcul=C3=A9=20?= =?UTF-8?q?pour=20des=20factures=20valides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/users/models.py b/users/models.py index 1852c687..148142a4 100644 --- a/users/models.py +++ b/users/models.py @@ -413,7 +413,8 @@ class User(FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin): somme_debit = Vente.objects.filter( facture__in=Facture.objects.filter( user=self, - paiement__in=solde_objects + paiement__in=solde_objects, + valid=True ) ).aggregate( total=models.Sum( @@ -422,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( From 5319ed266c3fe872237074d568fde4f61f66f286 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sat, 13 Jan 2018 15:53:53 +0100 Subject: [PATCH 04/25] Un peu de joli icone --- cotisations/templates/cotisations/recharge.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html index 6ca68696..d38b7614 100644 --- a/cotisations/templates/cotisations/recharge.html +++ b/cotisations/templates/cotisations/recharge.html @@ -29,8 +29,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}Rechargement du solde{% endblock %} {% block content %} -

Rechargement du solde

-

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

+

Rechargement du solde

+

Solde : {{ request.user.solde }} €

{% csrf_token %} {% bootstrap_form rechargeform %} From a71e01e18a478e763a2f47f873899bf8e8944573 Mon Sep 17 00:00:00 2001 From: Gabriel Detraz Date: Sun, 14 Jan 2018 20:15:21 +0100 Subject: [PATCH 05/25] Montant minimal de rechargement + refactor de la fonction payment + self adhesion --- cotisations/forms.py | 15 ++++++++++++++ cotisations/payment.py | 3 ++- cotisations/views.py | 4 ++-- preferences/forms.py | 2 ++ .../0031_optionaluser_self_adhesion.py | 20 +++++++++++++++++++ .../0032_optionaluser_min_online_payment.py | 20 +++++++++++++++++++ preferences/models.py | 9 +++++++++ .../preferences/display_preferences.html | 14 +++++++++---- 8 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 preferences/migrations/0031_optionaluser_self_adhesion.py create mode 100644 preferences/migrations/0032_optionaluser_min_online_payment.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 303ee32e..c4e02397 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -41,6 +41,8 @@ from django.db.models import Q from django.forms import ModelForm, Form 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 @@ -287,3 +289,16 @@ class RechargeForm(Form): 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 diff --git a/cotisations/payment.py b/cotisations/payment.py index ab7260b2..b2e684d7 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -76,7 +76,8 @@ def ipn(request): return HttpResponse("HTTP/1.0 200 OK") -def comnpay(facture, host): +def comnpay(facture, request): + host = request.get_host() p = ComnpayPayment( "DEMO", "DEMO", diff --git a/cotisations/views.py b/cotisations/views.py index 10e85ed0..24320b2b 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -687,7 +687,7 @@ def new_facture_solde(request, userid): @login_required def recharge(request): - f = RechargeForm(request.POST or None) + 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') @@ -702,6 +702,6 @@ def recharge(request): ) v.save() options, _created = AssoOption.objects.get_or_create() - content = payment.PAYMENT_SYSTEM[options.payment](facture, request.get_host()) + content = payment.PAYMENT_SYSTEM[options.payment](facture, request) return render(request, 'cotisations/payment.html', content) return form({'rechargeform':f}, 'cotisations/recharge.html', request) diff --git a/preferences/forms.py b/preferences/forms.py index 1fa42efc..03fa6360 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -49,6 +49,8 @@ class EditOptionalUserForm(ModelForm): 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): diff --git a/preferences/migrations/0031_optionaluser_self_adhesion.py b/preferences/migrations/0031_optionaluser_self_adhesion.py new file mode 100644 index 00000000..48a95044 --- /dev/null +++ b/preferences/migrations/0031_optionaluser_self_adhesion.py @@ -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'), + ), + ] diff --git a/preferences/migrations/0032_optionaluser_min_online_payment.py b/preferences/migrations/0032_optionaluser_min_online_payment.py new file mode 100644 index 00000000..ef78d012 --- /dev/null +++ b/preferences/migrations/0032_optionaluser_min_online_payment.py @@ -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), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 44bb6580..d4c25c24 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -46,11 +46,20 @@ class OptionalUser(models.Model): 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 = ( diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index d483cdb4..45a4fd11 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -54,11 +54,17 @@ 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 %} - Solde maximum - {{ useroptions.max_solde }} - {% endif %} + Auto inscription + {{ useroptions.self_adhesion }} + {% if useroptions.user_solde %} + + Solde maximum + {{ useroptions.max_solde }} + Montant minimal de rechargement en ligne + {{ useroptions.min_online_payment }} + + {% endif %}

Préférences machines

From b0fca7f6b6420ce1723d689fb4fc41553fc14222 Mon Sep 17 00:00:00 2001 From: Yoann Pietri Date: Sun, 14 Jan 2018 23:47:44 +0100 Subject: [PATCH 06/25] =?UTF-8?q?Impl=C3=A9mentation=20de=20l'auto=20inscr?= =?UTF-8?q?iption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preferences/forms.py | 1 + preferences/models.py | 10 ++++++++++ .../preferences/display_preferences.html | 6 ++++++ .../templates/preferences/edit_preferences.html | 2 +- preferences/views.py | 1 + re2o/acl.py | 5 ++--- re2o/settings.py | 2 +- templates/base.html | 15 +++++++++++++-- users/models.py | 11 +++++++---- users/templates/users/user.html | 4 ++++ users/views.py | 6 ++++-- 11 files changed, 50 insertions(+), 13 deletions(-) diff --git a/preferences/forms.py b/preferences/forms.py index 03fa6360..fc214f52 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -117,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): diff --git a/preferences/models.py b/preferences/models.py index d4c25c24..133d6f82 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -302,6 +302,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 = ( diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 45a4fd11..e46564a3 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -137,7 +137,13 @@ with this program; if not, write to the Free Software Foundation, Inc., Message global affiché sur le site {{ generaloptions.general_message }} + Résumé des CGU + {{ generaloptions.GTU_sum_up }} + + CGU + {{generaloptions.GTU}} +

Données de l'association

diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 02f006c1..8d75f289 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Edition des préférences

- + {% csrf_token %} {% massive_bootstrap_form options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} diff --git a/preferences/views.py b/preferences/views.py index c7ee5202..7a609a76 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -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(): diff --git a/re2o/acl.py b/re2o/acl.py index ffbbea42..23636ee4 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -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 diff --git a/re2o/settings.py b/re2o/settings.py index c342ef93..f88bd266 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -150,7 +150,7 @@ STATICFILES_DIRS = ( ), ) -MEDIA_ROOT = '/var/www/re2o/static' +MEDIA_ROOT = '/var/www/re2o/media' STATIC_URL = '/static/' diff --git a/templates/base.html b/templates/base.html index b3a9bb5d..0a0c3db0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -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 %} @@ -102,17 +104,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_view_app preferences %}