From 7d0f4d34cd7191324553329f59589a1fcd8c841e Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 16:31:47 +0200 Subject: [PATCH 01/38] Autorise le paiement de cotisation directement en ligne (sans passer par le solde) --- cotisations/payment.py | 15 +++++++++++++-- cotisations/views.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cotisations/payment.py b/cotisations/payment.py index b03e1c55..5a9f5432 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -25,13 +25,24 @@ def accept_payment(request, factureid): """ The view called when an online payment has been accepted. """ - facture = get_object_or_404(Facture, id=factureid) + invoice = get_object_or_404(Facture, id=factureid) messages.success( request, _("The payment of %(amount)s € has been accepted.") % { - 'amount': facture.prix() + 'amount': invoice.prix() } ) + # In case a cotisation was bought, inform the user, the + # cotisation time has been extended too + if any(purchase.type_cotisation for purchase 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() + } + ) return redirect(reverse( 'users:profil', kwargs={'userid': request.user.id} diff --git a/cotisations/views.py b/cotisations/views.py index 47076c8f..25b676c1 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -141,9 +141,14 @@ 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 = is_online_payment # Saving the invoice new_invoice_instance.save() + # Building a purchase for each article sold for art_item in articles: if art_item.cleaned_data: @@ -159,6 +164,12 @@ def new_facture(request, user, userid): ) new_purchase.save() + if is_online_payment: + content = online_payment.PAYMENT_SYSTEM[ + AssoOption.get_cached_value('payment') + ](invoice, request) + return render(request, 'cotisations/payment.html', content) + # 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 From d4383c22913a85659634fd8190a1243d2176ee53 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 17:33:49 +0200 Subject: [PATCH 02/38] =?UTF-8?q?Paiement=20de=20cotisation=20en=20ligne?= =?UTF-8?q?=20possible=20pour=20les=20utilisateurs=20normaux=20(d=C3=A9sac?= =?UTF-8?q?tivable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/forms.py | 6 +++++- .../0030_paiement_allow_self_subscription.py | 20 ++++++++++++++++++ cotisations/models.py | 21 +++++++++++++++++++ cotisations/views.py | 6 +++++- ...35_optionaluser_allow_self_subscription.py | 20 ++++++++++++++++++ preferences/models.py | 7 +++++++ users/templates/users/profil.html | 7 ++++--- 7 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 cotisations/migrations/0030_paiement_allow_self_subscription.py create mode 100644 preferences/migrations/0035_optionaluser_allow_self_subscription.py diff --git a/cotisations/forms.py b/cotisations/forms.py index a6647a45..d73ad6a3 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -55,9 +55,12 @@ class NewFactureForm(FormRevMixin, ModelForm): """ def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) + allowed_payment = kwargs.pop('allowed_payment', None) super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) # TODO : remove the use of cheque and banque and paiement # for something more generic or at least in English + if allowed_payment: + self.fields['paiement'].queryset = allowed_payment self.fields['cheque'].required = False self.fields['banque'].required = False self.fields['cheque'].label = _("Cheque number") @@ -69,6 +72,7 @@ class NewFactureForm(FormRevMixin, ModelForm): self.fields['paiement'].widget\ .attrs['data-cheque'] = paiement_list.first().id + class Meta: model = Facture fields = ['paiement', 'banque', 'cheque'] @@ -231,7 +235,7 @@ class PaiementForm(FormRevMixin, ModelForm): class Meta: model = Paiement # TODO : change moyen to method and type_paiement to payment_type - fields = ['moyen', 'type_paiement'] + fields = ['moyen', 'type_paiement', 'allow_self_subscription'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) diff --git a/cotisations/migrations/0030_paiement_allow_self_subscription.py b/cotisations/migrations/0030_paiement_allow_self_subscription.py new file mode 100644 index 00000000..4e9ab60b --- /dev/null +++ b/cotisations/migrations/0030_paiement_allow_self_subscription.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-17 14:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0029_auto_20180414_2056'), + ] + + operations = [ + migrations.AddField( + model_name='paiement', + name='allow_self_subscription', + field=models.BooleanField(default=False, verbose_name='Is available for self subscription'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index c4c6d4af..5c3a310a 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -46,6 +46,7 @@ from django.utils.translation import ugettext_lazy as _l from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin +from preferences.models import OptionalUser # TODO : change facture to invoice @@ -213,6 +214,22 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): _("You don't have the right to edit an invoice.") ) + @staticmethod + def can_create(user_request, *_args, **_kwargs): + """Check if an user can create an invoice. + + :param user_request: The user who wants to create an invoice. + :return: a message and a boolean which is True if the user can create + an invoice or if the `options.allow_self_subscription` is set. + """ + if OptionalUser.get_cached_value('allow_self_subscription'): + return True, None + return ( + user_request.has_perm('cotisations.add_facture'), + _("You don't have the right to create an invoice.") + ) + + def __init__(self, *args, **kwargs): super(Facture, self).__init__(*args, **kwargs) self.field_permissions = { @@ -576,6 +593,10 @@ class Paiement(RevMixin, AclMixin, models.Model): default=0, verbose_name=_l("Payment type") ) + allow_self_subscription = models.BooleanField( + default=False, + verbose_name=_l("Is available for self subscription") + ) class Meta: permissions = ( diff --git a/cotisations/views.py b/cotisations/views.py index 25b676c1..1b8fc80b 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -99,7 +99,11 @@ def new_facture(request, user, userid): Q(type_user='All') | Q(type_user=request.user.class_name) ) # Building the invocie form and the article formset - invoice_form = NewFactureForm(request.POST or None, instance=invoice) + if not request.user.has_perm('cotisations.add_facture') and OptionalUser.get_cached_value('allow_self_subscription'): + allowed_payment = Paiement.objects.filter(allow_self_subscription=True) + invoice_form = NewFactureForm(request.POST or None, instance=invoice, allowed_payment=allowed_payment) + else: + invoice_form = NewFactureForm(request.POST or None, instance=invoice) if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)( request.POST or None diff --git a/preferences/migrations/0035_optionaluser_allow_self_subscription.py b/preferences/migrations/0035_optionaluser_allow_self_subscription.py new file mode 100644 index 00000000..5fc45714 --- /dev/null +++ b/preferences/migrations/0035_optionaluser_allow_self_subscription.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-17 15:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0034_auto_20180416_1120'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='allow_self_subscription', + field=models.BooleanField(default=False, help_text="Autoriser les utilisateurs à cotiser par eux mêmes via les moyens de paiement permettant l'auto-cotisation."), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 560fa30f..eeae30f9 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -96,6 +96,13 @@ class OptionalUser(AclMixin, PreferencesModel): default=False, help_text="Un nouvel utilisateur peut se créer son compte sur re2o" ) + allow_self_subscription = models.BooleanField( + default=False, + help_text=( + "Autoriser les utilisateurs à cotiser par eux mêmes via les" + " moyens de paiement permettant l'auto-cotisation." + ) + ) shell_default = models.OneToOneField( 'users.ListShell', on_delete=models.PROTECT, diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index abba61a2..25b4992e 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -166,7 +166,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Solde {{ users.solde }} € - {% if allow_online_payment %} + {% if user_solde and allow_online_payment %} Recharger @@ -287,8 +287,9 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} {% if user_solde %} - Ajouter une cotisation par solde{% endif %}{% acl_end %} - + Ajouter une cotisation par solde + {% endif %} + {% acl_end %}
{% if facture_list %} From b92aeb7308f3fd88e626145240dcb2b0e150e911 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 17:38:01 +0200 Subject: [PATCH 03/38] Faire payer le bon prix, c'est mieux --- cotisations/payment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cotisations/payment.py b/cotisations/payment.py index 5a9f5432..48411c35 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -136,11 +136,11 @@ def comnpay(facture, request): 'action': 'https://secure.homologation.comnpay.com', 'method': 'POST', 'content': p.buildSecretHTML( - "Rechargement du solde", - facture.prix(), + "Paiement de la facture "+str(facture.id), + facture.prix_total(), idTransaction=str(facture.id) ), - 'amount': facture.prix, + 'amount': facture.prix_total(), } return r From 48d4c6f55ef005e4b5dd6f9d4a9c0b19739490c3 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 17:44:38 +0200 Subject: [PATCH 04/38] Afficher le bon prix c'est encore mieux --- cotisations/payment.py | 2 +- cotisations/templates/cotisations/new_facture.html | 2 ++ cotisations/templates/cotisations/payment.html | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cotisations/payment.py b/cotisations/payment.py index 48411c35..b1ca953e 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -29,7 +29,7 @@ def accept_payment(request, factureid): messages.success( request, _("The payment of %(amount)s € has been accepted.") % { - 'amount': invoice.prix() + 'amount': invoice.prix_total() } ) # In case a cotisation was bought, inform the user, the diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index 4dab92d3..6e90a12f 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -35,11 +35,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

{% trans "New invoice" %}

+ {% if user.solde %}

{% blocktrans %} User's balance : {{ user.solde }} € {% endblocktrans %}

+ {% endif %} {% bootstrap_form factureform %} {{ venteform.management_form }} diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html index a0a1abae..bfdde5c4 100644 --- a/cotisations/templates/cotisations/payment.html +++ b/cotisations/templates/cotisations/payment.html @@ -32,7 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% blocktrans %} - Refill of {{ amount }} € + Pay {{ amount }} € {% endblocktrans %}

From a1ec5eb831f8c1287c1b65278f5a890d71ef118b Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 21:49:59 +0200 Subject: [PATCH 05/38] Ne montre pas tous les articles en vente aux utilisateurs lambdas --- cotisations/forms.py | 16 +++++++++++++++ .../0031_article_allow_self_subscription.py | 20 +++++++++++++++++++ cotisations/models.py | 4 ++++ cotisations/views.py | 18 +++++++++++++++-- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 cotisations/migrations/0031_article_allow_self_subscription.py diff --git a/cotisations/forms.py b/cotisations/forms.py index d73ad6a3..a9f47f28 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -131,6 +131,14 @@ class SelectUserArticleForm( required=True ) + def __init__(self, *args, **kwargs): + self_subscription = kwargs.pop('is_self_subscription', False) + super(SelectUserArticleForm, self).__init__(*args, **kwargs) + if self_subscription: + self.fields['article'].queryset = Article.objects.filter( + Q(type_user='All') | Q(type_user='Adherent') + ).filter(allow_self_subscription=True) + class SelectClubArticleForm(Form): """ @@ -150,6 +158,14 @@ class SelectClubArticleForm(Form): required=True ) + def __init__(self, *args, **kwargs): + self_subscription = kwargs.pop('is_self_subscription', False) + super(SelectClubArticleForm, self).__init__(*args, **kwargs) + if self_subscription: + self.fields['article'].queryset = Article.objects.filter( + Q(type_user='All') | Q(type_user='Club') + ).filter(allow_self_subscription=True) + # TODO : change Facture to Invoice class NewFactureFormPdf(Form): diff --git a/cotisations/migrations/0031_article_allow_self_subscription.py b/cotisations/migrations/0031_article_allow_self_subscription.py new file mode 100644 index 00000000..64764edf --- /dev/null +++ b/cotisations/migrations/0031_article_allow_self_subscription.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-17 17:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0030_paiement_allow_self_subscription'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='allow_self_subscription', + field=models.BooleanField(default=False, verbose_name='Is available for self subscription'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 5c3a310a..fe741d8f 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -518,6 +518,10 @@ class Article(RevMixin, AclMixin, models.Model): max_length=255, verbose_name=_l("Type of cotisation") ) + allow_self_subscription = models.BooleanField( + default=False, + verbose_name=_l("Is available for self subscription") + ) unique_together = ('name', 'type_user') diff --git a/cotisations/views.py b/cotisations/views.py index 1b8fc80b..9d2b60a4 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -99,18 +99,32 @@ def new_facture(request, user, userid): Q(type_user='All') | Q(type_user=request.user.class_name) ) # Building the invocie form and the article formset + is_self_subscription = False if not request.user.has_perm('cotisations.add_facture') and OptionalUser.get_cached_value('allow_self_subscription'): + is_self_subscription = True + article_list = article_list.filter(allow_self_subscription=True) allowed_payment = Paiement.objects.filter(allow_self_subscription=True) invoice_form = NewFactureForm(request.POST or None, instance=invoice, allowed_payment=allowed_payment) + elif not OptionalUser.get_cached_value('allow_self_subscription'): + messages.error( + request, + _("You cannot subscribe. Please ask to the staff.") + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) else: invoice_form = NewFactureForm(request.POST or None, instance=invoice) if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)( - request.POST or None + request.POST or None, + form_kwargs={'is_self_subscription':is_self_subscription} ) else: article_formset = formset_factory(SelectUserArticleForm)( - request.POST or None + request.POST or None, + form_kwargs={'is_self_subscription':is_self_subscription} ) if invoice_form.is_valid() and article_formset.is_valid(): From 3861ce98706a70730eebd30029089ac09f36b098 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 23:09:55 +0200 Subject: [PATCH 06/38] Fix la validation de facture. --- cotisations/payment.py | 29 ++++++++++++++++------------- cotisations/views.py | 4 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cotisations/payment.py b/cotisations/payment.py index b1ca953e..0b673c66 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -26,23 +26,26 @@ def accept_payment(request, factureid): The view called when an online payment has been accepted. """ invoice = get_object_or_404(Facture, id=factureid) - messages.success( - request, - _("The payment of %(amount)s € has been accepted.") % { - 'amount': invoice.prix_total() - } - ) - # In case a cotisation was bought, inform the user, the - # cotisation time has been extended too - if any(purchase.type_cotisation for purchase in invoice.vente_set.all()): + if invoice.valid: 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() + _("The payment of %(amount)s € has been accepted.") % { + 'amount': invoice.prix_total() } ) + # In case a cotisation was bought, inform the user, the + # cotisation time has been extended too + if any(purchase.type_cotisation for purchase 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: + invoice.delete() return redirect(reverse( 'users:profil', kwargs={'userid': request.user.id} diff --git a/cotisations/views.py b/cotisations/views.py index 9d2b60a4..336d755f 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -162,7 +162,7 @@ def new_facture(request, user, userid): is_online_payment = new_invoice_instance.paiement == ( Paiement.objects.get_or_create( moyen='Rechargement en ligne')[0]) - new_invoice_instance.valid = is_online_payment + new_invoice_instance.valid = not is_online_payment # Saving the invoice new_invoice_instance.save() @@ -185,7 +185,7 @@ def new_facture(request, user, userid): if is_online_payment: content = online_payment.PAYMENT_SYSTEM[ AssoOption.get_cached_value('payment') - ](invoice, request) + ](new_invoice_instance, request) return render(request, 'cotisations/payment.html', content) # In case a cotisation was bought, inform the user, the From 6459345620e7208626d36076b78e0be43670e3ce Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 17 Jun 2018 16:31:47 +0200 Subject: [PATCH 07/38] Autorise le paiement de cotisation directement en ligne (sans passer par le solde) --- cotisations/payment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cotisations/payment.py b/cotisations/payment.py index 0b673c66..4749bd05 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -44,8 +44,6 @@ def accept_payment(request, factureid): 'end_date': request.user.end_adhesion() } ) - else: - invoice.delete() return redirect(reverse( 'users:profil', kwargs={'userid': request.user.id} From a6d9e01debc3363789c191335bb92212dfc36a65 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 18 Jun 2018 18:53:59 +0200 Subject: [PATCH 08/38] ReFix le AESField --- preferences/aes_field.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/preferences/aes_field.py b/preferences/aes_field.py index 302aa82b..1329b0a7 100644 --- a/preferences/aes_field.py +++ b/preferences/aes_field.py @@ -79,6 +79,12 @@ class AESEncryptedField(models.CharField): 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 From 73c0252214f205844db2095286ce11a5c5f548dc Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 18 Jun 2018 18:56:27 +0200 Subject: [PATCH 09/38] Refix les erreurs d'affichage --- cotisations/templates/cotisations/recharge.html | 2 +- cotisations/views.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html index 196de2ca..6f4e9d9c 100644 --- a/cotisations/templates/cotisations/recharge.html +++ b/cotisations/templates/cotisations/recharge.html @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "Balance refill" %}

{% blocktrans %} - Balance : {{ request.user.solde }} € + Balance : {{ solde }} € {% endblocktrans %}

diff --git a/cotisations/views.py b/cotisations/views.py index 336d755f..518df96b 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -899,5 +899,6 @@ def recharge(request): ](invoice, request) return render(request, 'cotisations/payment.html', content) return form({ - 'rechargeform': refill_form + 'rechargeform': refill_form, + 'solde': request.user.solde }, 'cotisations/recharge.html', request) From 3b2ca10e292ca86ae5bf98ed39883d659d24be58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Wed, 27 Jun 2018 12:21:20 +0000 Subject: [PATCH 10/38] Fix #109: Remove misnamed rights --- .../0083_remove_duplicate_rights.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 machines/migrations/0083_remove_duplicate_rights.py diff --git a/machines/migrations/0083_remove_duplicate_rights.py b/machines/migrations/0083_remove_duplicate_rights.py new file mode 100644 index 00000000..05ad2938 --- /dev/null +++ b/machines/migrations/0083_remove_duplicate_rights.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def remove_permission_alias(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + for codename in ['add_alias', 'change_alias', 'delete_alias']: + # Retrieve the wrong permission + try: + to_remove = Permission.objects.get( + codename=codename, + content_type__model='domain' + ) + except Permission.DoesNotExist: + # The permission is missing so no problem + pass + else: + to_remove.delete() + + +def remove_permission_text(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + for codename in ['add_text', 'change_text', 'delete_text']: + # Retrieve the wrong permission + try: + to_remove = Permission.objects.get( + codename=codename, + content_type__model='txt' + ) + except Permission.DoesNotExist: + # The permission is missing so no problem + pass + else: + to_remove.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0082_auto_20180525_2209'), + ] + + operations = [ + migrations.RunPython(remove_permission_text), + migrations.RunPython(remove_permission_alias), + ] From d9990899258934a3f3e09587baa00cfad13fbf49 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 21 Jun 2018 20:03:46 +0200 Subject: [PATCH 11/38] POC des moyens de paiements sous forme de modules. --- .../0032_chequepayment_comnpaypayment.py | 59 ++++++++++++ .../migrations/0033_auto_20180628_2157.py | 20 ++++ cotisations/models.py | 33 +++++++ cotisations/payment_methods/__init__.py | 9 ++ .../payment_methods/cheque/__init__.py | 7 ++ cotisations/payment_methods/cheque/forms.py | 10 ++ cotisations/payment_methods/cheque/models.py | 21 +++++ cotisations/payment_methods/cheque/urls.py | 10 ++ cotisations/payment_methods/cheque/views.py | 45 +++++++++ .../payment_methods/comnpay/__init__.py | 6 ++ .../payment_methods/comnpay/aes_field.py | 94 +++++++++++++++++++ .../comnpay}/comnpay.py | 2 +- cotisations/payment_methods/comnpay/models.py | 30 ++++++ cotisations/payment_methods/comnpay/urls.py | 20 ++++ .../comnpay/views.py} | 20 ++-- cotisations/payment_utils/__init__.py | 0 .../templates/cotisations/payment_form.html | 37 ++++++++ cotisations/urls.py | 20 +--- cotisations/views.py | 40 +------- users/views.py | 2 +- 20 files changed, 417 insertions(+), 68 deletions(-) create mode 100644 cotisations/migrations/0032_chequepayment_comnpaypayment.py create mode 100644 cotisations/migrations/0033_auto_20180628_2157.py create mode 100644 cotisations/payment_methods/__init__.py create mode 100644 cotisations/payment_methods/cheque/__init__.py create mode 100644 cotisations/payment_methods/cheque/forms.py create mode 100644 cotisations/payment_methods/cheque/models.py create mode 100644 cotisations/payment_methods/cheque/urls.py create mode 100644 cotisations/payment_methods/cheque/views.py create mode 100644 cotisations/payment_methods/comnpay/__init__.py create mode 100644 cotisations/payment_methods/comnpay/aes_field.py rename cotisations/{payment_utils => payment_methods/comnpay}/comnpay.py (99%) create mode 100644 cotisations/payment_methods/comnpay/models.py create mode 100644 cotisations/payment_methods/comnpay/urls.py rename cotisations/{payment.py => payment_methods/comnpay/views.py} (91%) delete mode 100644 cotisations/payment_utils/__init__.py create mode 100644 cotisations/templates/cotisations/payment_form.html diff --git a/cotisations/migrations/0032_chequepayment_comnpaypayment.py b/cotisations/migrations/0032_chequepayment_comnpaypayment.py new file mode 100644 index 00000000..76fc4ea0 --- /dev/null +++ b/cotisations/migrations/0032_chequepayment_comnpaypayment.py @@ -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), + ] diff --git a/cotisations/migrations/0033_auto_20180628_2157.py b/cotisations/migrations/0033_auto_20180628_2157.py new file mode 100644 index 00000000..51509105 --- /dev/null +++ b/cotisations/migrations/0033_auto_20180628_2157.py @@ -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), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index fe741d8f..d91aa6f1 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -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): """ diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py new file mode 100644 index 00000000..cc21f42b --- /dev/null +++ b/cotisations/payment_methods/__init__.py @@ -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')), +] diff --git a/cotisations/payment_methods/cheque/__init__.py b/cotisations/payment_methods/cheque/__init__.py new file mode 100644 index 00000000..6610cdd8 --- /dev/null +++ b/cotisations/payment_methods/cheque/__init__.py @@ -0,0 +1,7 @@ +""" +This module contains a method to pay online using cheque. +""" +from . import models, urls, views +NAME = "CHEQUE" + +Payment = models.ChequePayment diff --git a/cotisations/payment_methods/cheque/forms.py b/cotisations/payment_methods/cheque/forms.py new file mode 100644 index 00000000..892ba190 --- /dev/null +++ b/cotisations/payment_methods/cheque/forms.py @@ -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")) diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py new file mode 100644 index 00000000..336f025e --- /dev/null +++ b/cotisations/payment_methods/cheque/models.py @@ -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} + )) diff --git a/cotisations/payment_methods/cheque/urls.py b/cotisations/payment_methods/cheque/urls.py new file mode 100644 index 00000000..f7cc39d5 --- /dev/null +++ b/cotisations/payment_methods/cheque/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from . import views + +urlpatterns = [ + url( + r'^validate/(?P[0-9]+)$', + views.cheque, + name='validate' + ) +] diff --git a/cotisations/payment_methods/cheque/views.py b/cotisations/payment_methods/cheque/views.py new file mode 100644 index 00000000..ee858c9b --- /dev/null +++ b/cotisations/payment_methods/cheque/views.py @@ -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} + ) diff --git a/cotisations/payment_methods/comnpay/__init__.py b/cotisations/payment_methods/comnpay/__init__.py new file mode 100644 index 00000000..d0289364 --- /dev/null +++ b/cotisations/payment_methods/comnpay/__init__.py @@ -0,0 +1,6 @@ +""" +This module contains a method to pay online using comnpay. +""" +from . import models, urls, views +NAME = "COMNPAY" +Payment = models.ComnpayPayment diff --git a/cotisations/payment_methods/comnpay/aes_field.py b/cotisations/payment_methods/comnpay/aes_field.py new file mode 100644 index 00000000..1329b0a7 --- /dev/null +++ b/cotisations/payment_methods/comnpay/aes_field.py @@ -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 + )) diff --git a/cotisations/payment_utils/comnpay.py b/cotisations/payment_methods/comnpay/comnpay.py similarity index 99% rename from cotisations/payment_utils/comnpay.py rename to cotisations/payment_methods/comnpay/comnpay.py index 6c73463f..272ab928 100644 --- a/cotisations/payment_utils/comnpay.py +++ b/cotisations/payment_methods/comnpay/comnpay.py @@ -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 """ diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py new file mode 100644 index 00000000..83a6f534 --- /dev/null +++ b/cotisations/payment_methods/comnpay/models.py @@ -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) diff --git a/cotisations/payment_methods/comnpay/urls.py b/cotisations/payment_methods/comnpay/urls.py new file mode 100644 index 00000000..d7b1e293 --- /dev/null +++ b/cotisations/payment_methods/comnpay/urls.py @@ -0,0 +1,20 @@ +from django.conf.urls import url +from . import views + +urlpatterns = [ + url( + r'^accept/(?P[0-9]+)$', + views.accept_payment, + name='accept_payment' + ), + url( + r'^refuse/$', + views.refuse_payment, + name='refuse_payment' + ), + url( + r'^ipn/$', + views.ipn, + name='ipn' + ), +] diff --git a/cotisations/payment.py b/cotisations/payment_methods/comnpay/views.py similarity index 91% rename from cotisations/payment.py rename to cotisations/payment_methods/comnpay/views.py index 4749bd05..094ebff4 100644 --- a/cotisations/payment.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -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 -} diff --git a/cotisations/payment_utils/__init__.py b/cotisations/payment_utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cotisations/templates/cotisations/payment_form.html b/cotisations/templates/cotisations/payment_form.html new file mode 100644 index 00000000..1057b7cb --- /dev/null +++ b/cotisations/templates/cotisations/payment_form.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 © 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 %} +

{% trans form.title %}

+ + {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %} + +{% endblock %} diff --git a/cotisations/urls.py b/cotisations/urls.py index 6eafe721..0603c96e 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -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[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 + diff --git a/cotisations/views.py b/cotisations/views.py index 518df96b..47b716ce 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -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, diff --git a/users/views.py b/users/views.py index fcb44f65..a125124c 100644 --- a/users/views.py +++ b/users/views.py @@ -882,7 +882,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', From 32df64253bc5ca7f00413475bf383d0c3646fcc4 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 2 Jul 2018 14:09:35 +0200 Subject: [PATCH 12/38] =?UTF-8?q?Autorise=20l'=C3=A9dition=20de=20facture?= =?UTF-8?q?=20si=20l'auto-souscription=20est=20interdite=20mais=20que=20l'?= =?UTF-8?q?on=20peut=20cr=C3=A9er=20des=20factures.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/views.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/cotisations/views.py b/cotisations/views.py index 47b716ce..ea137379 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -99,22 +99,26 @@ def new_facture(request, user, userid): ) # Building the invocie form and the article formset is_self_subscription = False - if not request.user.has_perm('cotisations.add_facture') and OptionalUser.get_cached_value('allow_self_subscription'): - is_self_subscription = True - article_list = article_list.filter(allow_self_subscription=True) - allowed_payment = Paiement.objects.filter(allow_self_subscription=True) - invoice_form = NewFactureForm(request.POST or None, instance=invoice, allowed_payment=allowed_payment) - elif not OptionalUser.get_cached_value('allow_self_subscription'): - messages.error( - request, - _("You cannot subscribe. Please ask to the staff.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) + can_create_invoice = request.user.has_perm('cotisations.add_facture') + allow_self_subscription = OptionalUser.get_cached_value('allow_self_subscription') + if not can_create_invoice: + if allow_self_subscription: + is_self_subscription = True + article_list = article_list.filter(allow_self_subscription=True) + allowed_payment = Paiement.objects.filter(allow_self_subscription=True) + invoice_form = NewFactureForm(request.POST or None, instance=invoice, allowed_payment=allowed_payment) + else: + messages.error( + request, + _("You cannot subscribe. Please ask to the staff.") + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) else: invoice_form = NewFactureForm(request.POST or None, instance=invoice) + if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)( request.POST or None, From a756f85186331ca5181a8044558fc1b2f12d7a8a Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 2 Jul 2018 14:24:53 +0200 Subject: [PATCH 13/38] =?UTF-8?q?Retire=20les=20=C3=A9l=C3=A9ments=20de=20?= =?UTF-8?q?front=20en=20JS=20pour=20les=20ch=C3=A8ques.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/forms.py | 11 +---------- .../templates/cotisations/new_facture.html | 18 ------------------ 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index a9f47f28..a5db7b92 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -61,21 +61,12 @@ class NewFactureForm(FormRevMixin, ModelForm): # for something more generic or at least in English if allowed_payment: self.fields['paiement'].queryset = allowed_payment - self.fields['cheque'].required = False - self.fields['banque'].required = False - self.fields['cheque'].label = _("Cheque number") - self.fields['banque'].empty_label = _("Not specified") self.fields['paiement'].empty_label = \ _("Select a payment method") - 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', 'cheque'] + fields = ['paiement'] def clean(self): cleaned_data = super(NewFactureForm, self).clean() diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index 6e90a12f..ddb1ece8 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -44,7 +44,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% bootstrap_form factureform %} {{ venteform.management_form }} -

{% trans "Invoice's articles" %}

{% for form in venteform.forms %} @@ -132,20 +131,6 @@ with this program; if not, write to the Free Software Foundation, Inc., ) } - function set_cheque_info_visibility() { - var paiement = document.getElementById("id_Facture-paiement"); - var visible = paiement.value == paiement.getAttribute('data-cheque'); - p = document.getElementById("id_Facture-paiement"); - var display = 'none'; - if (visible) { - display = 'block'; - } - document.getElementById("id_Facture-cheque") - .parentNode.style.display = display; - document.getElementById("id_Facture-banque") - .parentNode.style.display = display; - } - // Add events manager when DOM is fully loaded document.addEventListener("DOMContentLoaded", function() { document.getElementById("add_one") @@ -155,9 +140,6 @@ with this program; if not, write to the Free Software Foundation, Inc., for (i = 0; i < product_count; ++i){ add_listenner_for_id(i); } - document.getElementById("id_Facture-paiement") - .addEventListener("change", set_cheque_info_visibility, true); - set_cheque_info_visibility(); update_price(); }); From 57c8411d7ed03bfa8381a8b9b55dc00c928fd127 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 2 Jul 2018 14:41:02 +0200 Subject: [PATCH 14/38] =?UTF-8?q?Utilise=20un=20ModelForm=20pour=20les=20d?= =?UTF-8?q?onn=C3=A9es=20de=20ch=C3=A8que=20+=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/models.py | 48 ++++++++++++--------- cotisations/payment_methods/cheque/views.py | 19 ++++---- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index d91aa6f1..d314f31d 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -135,7 +135,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ price = Vente.objects.filter( facture=self - ).aggregate(models.Sum('prix'))['prix__sum'] + ).aggregate(models.Sum('prix'))['prix__sum'] return price # TODO : change prix to price @@ -147,12 +147,12 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): # TODO : change Vente to somethingelse return Vente.objects.filter( facture=self - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] def name(self): """ @@ -161,7 +161,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ name = ' - '.join(Vente.objects.filter( facture=self - ).values_list('name', flat=True)) + ).values_list('name', flat=True)) return name def can_edit(self, user_request, *args, **kwargs): @@ -232,7 +232,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): _("You don't have the right to create an invoice.") ) - def __init__(self, *args, **kwargs): super(Facture, self).__init__(*args, **kwargs) self.field_permissions = { @@ -361,12 +360,12 @@ class Vente(RevMixin, AclMixin, models.Model): facture__in=Facture.objects.filter( user=self.facture.user ).exclude(valid=False)) - ).filter( - Q(type_cotisation='All') | - Q(type_cotisation=self.type_cotisation) - ).filter( - date_start__lt=date_start - ).aggregate(Max('date_end'))['date_end__max'] + ).filter( + Q(type_cotisation='All') | + Q(type_cotisation=self.type_cotisation) + ).filter( + date_start__lt=date_start + ).aggregate(Max('date_end'))['date_end__max'] elif self.type_cotisation == "Adhesion": end_cotisation = self.facture.user.end_adhesion() else: @@ -377,7 +376,7 @@ class Vente(RevMixin, AclMixin, models.Model): cotisation.date_start = date_max cotisation.date_end = cotisation.date_start + relativedelta( months=self.duration*self.number - ) + ) return def save(self, *args, **kwargs): @@ -400,7 +399,7 @@ class Vente(RevMixin, AclMixin, models.Model): elif (not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit( user_request, *args, **kwargs - )[0]): + )[0]): return False, _("You don't have the right to edit this user's " "purchases.") elif (not user_request.has_perm('cotisations.change_all_vente') and @@ -632,12 +631,19 @@ class Paiement(RevMixin, AclMixin, models.Model): ) super(Paiement, self).save(*args, **kwargs) - def end_payment(self, invoice, request): + def end_payment(self, invoice, request, use_payment_method=True): """ - The general way of ending a payment. You may redefine this method for custom - payment methods. Must return a HttpResponse-like object. + The general way of ending a payment. + + :param invoice: The invoice being created. + :param request: Request sended by the user. + :param use_payment_method: If `self` has an attribute `payment_method`, + returns the result of + `self.payment_method.end_payment(invoice, request)` + + :returns: An `HttpResponse`-like object. """ - if hasattr(self, 'payment_method'): + if hasattr(self, 'payment_method') and use_payment_method: return self.payment_method.end_payment(invoice, request) # In case a cotisation was bought, inform the user, the diff --git a/cotisations/payment_methods/cheque/views.py b/cotisations/payment_methods/cheque/views.py index ee858c9b..cb009deb 100644 --- a/cotisations/payment_methods/cheque/views.py +++ b/cotisations/payment_methods/cheque/views.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext as _ from cotisations.models import Facture as Invoice from .models import ChequePayment -from .forms import ChequeForm +from .forms import InvoiceForm @login_required @@ -28,16 +28,15 @@ def cheque(request, invoice_pk): 'users:profil', kwargs={'userid': request.user.pk} )) - form = ChequeForm(request.POST or None) + form = InvoiceForm(request.POST or None, instance=invoice) 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} - )) + form.instance.valid = True + form.save() + return form.instance.paiement.end_payment( + form.instance, + request, + use_payment_method=False + ) return render( request, 'cotisations/payment_form.html', From 10a54639b6a5f0e975727b5b1f90157df4c95c2e Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 2 Jul 2018 15:01:34 +0200 Subject: [PATCH 15/38] Invalide la facture Comnpay avant de continuer le paiement. --- cotisations/payment_methods/comnpay/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index 83a6f534..4fc509af 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -26,5 +26,7 @@ class ComnpayPayment(models.Model): ) def end_payment(self, invoice, request): + invoice.valid = False + invoice.save() content = comnpay(invoice, request) return render(request, 'cotisations/payment.html', content) From bfefca68323f40bf539b729c579cb4e802979e72 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 2 Jul 2018 21:13:13 +0200 Subject: [PATCH 16/38] =?UTF-8?q?Permet=20la=20cr=C3=A9ation=20et=20l'?= =?UTF-8?q?=C3=A9dition=20de=20moyen=20de=20paiement=20personnalis=C3=A9s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/forms.py | 7 +-- .../0032_chequepayment_comnpaypayment.py | 15 +++-- .../migrations/0033_auto_20180628_2157.py | 20 ------- cotisations/payment_methods/__init__.py | 16 +++++ .../payment_methods/cheque/__init__.py | 2 +- cotisations/payment_methods/cheque/forms.py | 10 ++-- cotisations/payment_methods/cheque/models.py | 15 +++-- .../payment_methods/comnpay/__init__.py | 2 +- cotisations/payment_methods/comnpay/models.py | 15 +++-- cotisations/payment_methods/forms.py | 59 +++++++++++++++++++ cotisations/payment_methods/mixins.py | 21 +++++++ .../templates/cotisations/facture.html | 37 +++++++++++- cotisations/views.py | 41 +++++++++---- 13 files changed, 198 insertions(+), 62 deletions(-) delete mode 100644 cotisations/migrations/0033_auto_20180628_2157.py create mode 100644 cotisations/payment_methods/forms.py create mode 100644 cotisations/payment_methods/mixins.py diff --git a/cotisations/forms.py b/cotisations/forms.py index a5db7b92..0ee4c143 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -242,17 +242,12 @@ class PaiementForm(FormRevMixin, ModelForm): class Meta: model = Paiement # TODO : change moyen to method and type_paiement to payment_type - fields = ['moyen', 'type_paiement', 'allow_self_subscription'] + fields = ['moyen', 'allow_self_subscription'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['moyen'].label = _("Payment method name") - self.fields['type_paiement'].label = _("Payment type") - self.fields['type_paiement'].help_text = \ - _("The payement type is used for specific behaviour.\ - The \"cheque\" type means a cheque number and a bank name\ - may be added when using this payment method.") # TODO : change paiement to payment diff --git a/cotisations/migrations/0032_chequepayment_comnpaypayment.py b/cotisations/migrations/0032_chequepayment_comnpaypayment.py index 76fc4ea0..d2c67308 100644 --- a/cotisations/migrations/0032_chequepayment_comnpaypayment.py +++ b/cotisations/migrations/0032_chequepayment_comnpaypayment.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2018-06-28 17:28 +# Generated by Django 1.10.7 on 2018-07-02 18:56 from __future__ import unicode_literals import cotisations.payment_methods.comnpay.aes_field +import cotisations.payment_methods.models from django.db import migrations, models import django.db.models.deletion @@ -29,6 +30,8 @@ def add_comnpay(apps, schema_editor): comnpay.payment_pass = options.payment_pass comnpay.payment = payment comnpay.save() + payment.moyen = "ComnPay" + payment.save() class Migration(migrations.Migration): @@ -42,18 +45,20 @@ class Migration(migrations.Migration): 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')), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), ], + bases=(cotisations.payment_methods.models.PaymentMethodMixin, models.Model), ), 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_credential', 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')), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), ], + bases=(cotisations.payment_methods.models.PaymentMethodMixin, models.Model), ), - migrations.RunPython(add_cheque), migrations.RunPython(add_comnpay), + migrations.RunPython(add_cheque), ] diff --git a/cotisations/migrations/0033_auto_20180628_2157.py b/cotisations/migrations/0033_auto_20180628_2157.py deleted file mode 100644 index 51509105..00000000 --- a/cotisations/migrations/0033_auto_20180628_2157.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- 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), - ), - ] diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py index cc21f42b..36904411 100644 --- a/cotisations/payment_methods/__init__.py +++ b/cotisations/payment_methods/__init__.py @@ -1,4 +1,5 @@ from django.conf.urls import include, url +from django.utils.translation import ugettext_lazy as _l from . import comnpay, cheque @@ -7,3 +8,18 @@ urlpatterns = [ url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')), url(r'^cheque/', include(cheque.urls, namespace='cheque')), ] + +PAYMENT_METHODS = [ + comnpay, + cheque, +] + + +def find_payment_method(payment): + for method in PAYMENT_METHODS: + try: + o = method.PaymentMethod.objects.get(payment=payment) + return o + except method.PaymentMethod.DoesNotExist: + pass + return None diff --git a/cotisations/payment_methods/cheque/__init__.py b/cotisations/payment_methods/cheque/__init__.py index 6610cdd8..a6fef640 100644 --- a/cotisations/payment_methods/cheque/__init__.py +++ b/cotisations/payment_methods/cheque/__init__.py @@ -4,4 +4,4 @@ This module contains a method to pay online using cheque. from . import models, urls, views NAME = "CHEQUE" -Payment = models.ChequePayment +PaymentMethod = models.ChequePayment diff --git a/cotisations/payment_methods/cheque/forms.py b/cotisations/payment_methods/cheque/forms.py index 892ba190..ae816a4b 100644 --- a/cotisations/payment_methods/cheque/forms.py +++ b/cotisations/payment_methods/cheque/forms.py @@ -1,10 +1,12 @@ from django import forms from django.utils.translation import ugettext_lazy as _l -from cotisations.models import Banque as Bank +from re2o.mixins import FormRevMixin +from cotisations.models import Facture as Invoice -class ChequeForm(forms.Form): +class InvoiceForm(FormRevMixin, forms.ModelForm): """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")) + class Meta: + model = Invoice + fields = ['banque', 'cheque'] diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py index 336f025e..29c86a41 100644 --- a/cotisations/payment_methods/cheque/models.py +++ b/cotisations/payment_methods/cheque/models.py @@ -2,16 +2,19 @@ from django.db import models from django.shortcuts import redirect from django.urls import reverse -from cotisations.models import Paiement as BasePayment +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin -class ChequePayment(models.Model): +class ChequePayment(PaymentMethodMixin, models.Model): """ - The model allowing you to pay with a cheque. It redefines post_payment - method. See `cotisations.models.Paiement for further details. + The model allowing you to pay with a cheque. """ - payment = models.OneToOneField(BasePayment, related_name='payment_method') - + payment = models.OneToOneField( + Paiement, + related_name='payment_method', + editable=False + ) def end_payment(self, invoice, request): invoice.valid = False invoice.save() diff --git a/cotisations/payment_methods/comnpay/__init__.py b/cotisations/payment_methods/comnpay/__init__.py index d0289364..58433c80 100644 --- a/cotisations/payment_methods/comnpay/__init__.py +++ b/cotisations/payment_methods/comnpay/__init__.py @@ -3,4 +3,4 @@ This module contains a method to pay online using comnpay. """ from . import models, urls, views NAME = "COMNPAY" -Payment = models.ComnpayPayment +PaymentMethod = models.ComnpayPayment diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index 4fc509af..68f883d4 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -1,19 +1,22 @@ from django.db import models from django.shortcuts import render -from cotisations.models import Paiement as BasePayment +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin from .aes_field import AESEncryptedField from .views import comnpay -class ComnpayPayment(models.Model): +class ComnpayPayment(PaymentMethodMixin, models.Model): """ - The model allowing you to pay with COMNPAY. It redefines post_payment - method. See `cotisations.models.Paiement for further details. + The model allowing you to pay with COMNPAY. """ - payment = models.OneToOneField(BasePayment, related_name='payment_method') - + payment = models.OneToOneField( + Paiement, + related_name='payment_method', + editable=False + ) payment_credential = models.CharField( max_length=255, default='', diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py new file mode 100644 index 00000000..2acc80f0 --- /dev/null +++ b/cotisations/payment_methods/forms.py @@ -0,0 +1,59 @@ +from django import forms +from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _l + +from . import PAYMENT_METHODS, find_payment_method + + +def payment_method_factory(payment, *args, **kwargs): + payment_method = kwargs.pop('instance', find_payment_method(payment)) + if payment_method is not None: + return forms.modelform_factory(type(payment_method), fields='__all__')( + *args, + instance=payment_method, + **kwargs + ) + return PaymentMethodForm(payment_method, *args, **kwargs) + + +class PaymentMethodForm(forms.Form): + """A special form which allows you to add a payment method to a `Payment` + objects if it hasn't one yet, or to edit the existing payment method. + + To do so it replaces itself with a `modelform_factory`. + """ + + payment_method = forms.ChoiceField( + label=_l("Special payment method"), + required=False + ) + + def __init__(self, payment_method, *args, **kwargs): + super(PaymentMethodForm, self).__init__(*args, **kwargs) + if payment_method is None: + prefix = kwargs.get('prefix', None) + self.fields['payment_method'].choices = [(i,p.NAME) for (i,p) in enumerate(PAYMENT_METHODS)] + self.fields['payment_method'].choices.insert(0, ('', _l('no'))) + self.fields['payment_method'].widget.attrs = { + 'id': 'paymentMethodSelect' + } + self.templates = [ + forms.modelform_factory(p.PaymentMethod, fields='__all__')(prefix=prefix) + for p in PAYMENT_METHODS + ] + else: + self.fields = {} + + def save(self, *args, payment=None, **kwargs): + commit = kwargs.pop('commit', True) + choice = self.cleaned_data['payment_method'] + if choice=='': + return + choice = int(choice) + model = PAYMENT_METHODS[choice].PaymentMethod + form = forms.modelform_factory(model, fields='__all__')(self.data, prefix=self.prefix) + payment_method = form.save(commit=False) + payment_method.payment = payment + if commit: + payment_method.save() + return payment_method diff --git a/cotisations/payment_methods/mixins.py b/cotisations/payment_methods/mixins.py new file mode 100644 index 00000000..62ebbf32 --- /dev/null +++ b/cotisations/payment_methods/mixins.py @@ -0,0 +1,21 @@ +from django.db import models + +from cotisations.models import Paiement + + +class PaymentMethodMixin: + """The base class for payment models. They should inherit from this.""" + payment = models.OneToOneField( + Paiement, + related_name='payment_method', + editable=False + ) + + def end_payment(self, invoice, request): + """Redefine this method in order to get a different ending to the + payment session if you whish. + + Must return a HttpResponse-like object. + """ + return self.payment.end_payment( + invoice, request, use_payment_method=False) diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index dc8648ac..082759c4 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -57,16 +57,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% endif %} {% bootstrap_form factureform %} + {% if payment_method %} + {% bootstrap_form payment_method %} +
+ {% endif %} {% bootstrap_button action_name button_type='submit' icon='star' %} -{% if articlesformset %} +{% if articlesformset or payment_method%} {% endif %} diff --git a/cotisations/views.py b/cotisations/views.py index ea137379..280a3aa9 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -74,6 +74,7 @@ from .forms import ( RechargeForm ) from .tex import render_invoice +from .payment_methods.forms import payment_method_factory @login_required @@ -473,9 +474,15 @@ def add_paiement(request): """ View used to add a payment method. """ - payment = PaiementForm(request.POST or None) - if payment.is_valid(): - payment.save() + payment = PaiementForm(request.POST or None, prefix='payment') + payment_method = payment_method_factory( + payment.instance, + request.POST or None, + prefix='payment_method' + ) + if payment.is_valid() and payment_method.is_valid(): + payment = payment.save() + payment_method.save(payment=payment) messages.success( request, _("The payment method has been successfully created.") @@ -483,6 +490,7 @@ def add_paiement(request): return redirect(reverse('cotisations:index-paiement')) return form({ 'factureform': payment, + 'payment_method': payment_method, 'action_name': _("Add") }, 'cotisations/facture.html', request) @@ -494,17 +502,28 @@ def edit_paiement(request, paiement_instance, **_kwargs): """ View used to edit a payment method. """ - payment = PaiementForm(request.POST or None, instance=paiement_instance) - if payment.is_valid(): - if payment.changed_data: - payment.save() - messages.success( - request, - _("The payement method has been successfully edited.") - ) + payment = PaiementForm( + request.POST or None, + instance=paiement_instance, + prefix="payment" + ) + payment_method = payment_method_factory( + paiement_instance, + request.POST or None, + prefix='payment_method' + ) + + if payment.is_valid() and payment_method.is_valid(): + payment.save() + payment_method.save() + messages.success( + request, + _("The payement method has been successfully edited.") + ) return redirect(reverse('cotisations:index-paiement')) return form({ 'factureform': payment, + 'payment_method': payment_method, 'action_name': _("Edit") }, 'cotisations/facture.html', request) From 05bc31709ac9f54633c46ffd069dd748f789aa5f Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 2 Jul 2018 22:14:51 +0200 Subject: [PATCH 17/38] =?UTF-8?q?Ajout=20de=20comnpay=20+=20fix=20de=20l'u?= =?UTF-8?q?tilisation=20des=20paiements=20personnalis=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/models.py | 7 +++++-- cotisations/payment_methods/__init__.py | 21 +------------------ cotisations/payment_methods/comnpay/models.py | 2 +- cotisations/payment_methods/comnpay/views.py | 12 +++++------ cotisations/payment_methods/forms.py | 4 ++-- cotisations/payment_methods/urls.py | 7 +++++++ cotisations/urls.py | 2 +- cotisations/utils.py | 9 ++++++++ 8 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 cotisations/payment_methods/urls.py create mode 100644 cotisations/utils.py diff --git a/cotisations/models.py b/cotisations/models.py index d314f31d..17d8cc2a 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -51,6 +51,8 @@ from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin from preferences.models import OptionalUser +from cotisations.utils import find_payment_method + # TODO : change facture to invoice class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @@ -643,8 +645,9 @@ class Paiement(RevMixin, AclMixin, models.Model): :returns: An `HttpResponse`-like object. """ - if hasattr(self, 'payment_method') and use_payment_method: - return self.payment_method.end_payment(invoice, request) + payment_method = find_payment_method(self) + if payment_method is not None and use_payment_method: + return payment_method.end_payment(invoice, request) # In case a cotisation was bought, inform the user, the # cotisation time has been extended too diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py index 36904411..a85fa49c 100644 --- a/cotisations/payment_methods/__init__.py +++ b/cotisations/payment_methods/__init__.py @@ -1,25 +1,6 @@ -from django.conf.urls import include, url -from django.utils.translation import ugettext_lazy as _l - -from . import comnpay, cheque - - -urlpatterns = [ - url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')), - url(r'^cheque/', include(cheque.urls, namespace='cheque')), -] +from . import comnpay, cheque, urls PAYMENT_METHODS = [ comnpay, cheque, ] - - -def find_payment_method(payment): - for method in PAYMENT_METHODS: - try: - o = method.PaymentMethod.objects.get(payment=payment) - return o - except method.PaymentMethod.DoesNotExist: - pass - return None diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index 68f883d4..466f6d14 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -31,5 +31,5 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): def end_payment(self, invoice, request): invoice.valid = False invoice.save() - content = comnpay(invoice, request) + content = comnpay(invoice, request, self) return render(request, 'cotisations/payment.html', content) diff --git a/cotisations/payment_methods/comnpay/views.py b/cotisations/payment_methods/comnpay/views.py index 094ebff4..85b237ff 100644 --- a/cotisations/payment_methods/comnpay/views.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -114,7 +114,7 @@ def ipn(request): return HttpResponse("HTTP/1.0 200 OK") -def comnpay(facture, request): +def comnpay(facture, request, payment): """ Build a request to start the negociation with Comnpay by using a facture id, the price and the secret transaction data stored in @@ -122,14 +122,14 @@ def comnpay(facture, request): """ host = request.get_host() p = Transaction( - str(AssoOption.get_cached_value('payment_id')), - str(AssoOption.get_cached_value('payment_pass')), + str(payment.payment_credential), + str(payment.payment_pass), 'https://' + host + reverse( - 'cotisations:comnpay_accept_payment', + 'cotisations:comnpay:accept_payment', kwargs={'factureid': facture.id} ), - 'https://' + host + reverse('cotisations:comnpay_refuse_payment'), - 'https://' + host + reverse('cotisations:comnpay_ipn'), + 'https://' + host + reverse('cotisations:comnpay:refuse_payment'), + 'https://' + host + reverse('cotisations:comnpay:ipn'), "", "D" ) diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py index 2acc80f0..f5bf4f76 100644 --- a/cotisations/payment_methods/forms.py +++ b/cotisations/payment_methods/forms.py @@ -2,8 +2,8 @@ from django import forms from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l -from . import PAYMENT_METHODS, find_payment_method - +from . import PAYMENT_METHODS +from cotisations.utils import find_payment_method def payment_method_factory(payment, *args, **kwargs): payment_method = kwargs.pop('instance', find_payment_method(payment)) diff --git a/cotisations/payment_methods/urls.py b/cotisations/payment_methods/urls.py new file mode 100644 index 00000000..9d35846f --- /dev/null +++ b/cotisations/payment_methods/urls.py @@ -0,0 +1,7 @@ +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')), +] diff --git a/cotisations/urls.py b/cotisations/urls.py index 0603c96e..9e318b53 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -144,5 +144,5 @@ urlpatterns = [ name='recharge' ), url(r'^$', views.index, name='index'), -] + payment_methods.urlpatterns +] + payment_methods.urls.urlpatterns diff --git a/cotisations/utils.py b/cotisations/utils.py new file mode 100644 index 00000000..01881af3 --- /dev/null +++ b/cotisations/utils.py @@ -0,0 +1,9 @@ +def find_payment_method(payment): + from cotisations.payment_methods import PAYMENT_METHODS + for method in PAYMENT_METHODS: + try: + o = method.PaymentMethod.objects.get(payment=payment) + return o + except method.PaymentMethod.DoesNotExist: + pass + return None From 78754b4b9d919b1fb316302ed84827d7fce48824 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 13:58:10 +0200 Subject: [PATCH 18/38] Typos, pep8 et company. --- cotisations/models.py | 2 +- cotisations/views.py | 68 ++++++++++++++++--------------- users/templates/users/profil.html | 3 +- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index 17d8cc2a..66e045db 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -221,7 +221,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_create(user_request, *_args, **_kwargs): - """Check if an user can create an invoice. + """Check if a user can create an invoice. :param user_request: The user who wants to create an invoice. :return: a message and a boolean which is True if the user can create diff --git a/cotisations/views.py b/cotisations/views.py index 280a3aa9..0acd6b78 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -101,16 +101,19 @@ def new_facture(request, user, userid): # Building the invocie form and the article formset is_self_subscription = False can_create_invoice = request.user.has_perm('cotisations.add_facture') - allow_self_subscription = OptionalUser.get_cached_value('allow_self_subscription') + allow_self_subscription = OptionalUser.get_cached_value( + 'allow_self_subscription') if not can_create_invoice: if allow_self_subscription: is_self_subscription = True article_list = article_list.filter(allow_self_subscription=True) - allowed_payment = Paiement.objects.filter(allow_self_subscription=True) - invoice_form = NewFactureForm(request.POST or None, instance=invoice, allowed_payment=allowed_payment) + allowed_payment = Paiement.objects.filter( + allow_self_subscription=True) + invoice_form = NewFactureForm( + request.POST or None, instance=invoice, allowed_payment=allowed_payment) else: messages.error( - request, + request, _("You cannot subscribe. Please ask to the staff.") ) return redirect(reverse( @@ -123,12 +126,12 @@ def new_facture(request, user, userid): if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)( request.POST or None, - form_kwargs={'is_self_subscription':is_self_subscription} + form_kwargs={'is_self_subscription': is_self_subscription} ) else: article_formset = formset_factory(SelectUserArticleForm)( request.POST or None, - form_kwargs={'is_self_subscription':is_self_subscription} + form_kwargs={'is_self_subscription': is_self_subscription} ) if invoice_form.is_valid() and article_formset.is_valid(): @@ -165,7 +168,6 @@ def new_facture(request, user, userid): )) new_invoice_instance.save() - # Building a purchase for each article sold for art_item in articles: if art_item.cleaned_data: @@ -253,13 +255,13 @@ def new_facture_pdf(request): 'email': AssoOption.get_cached_value('contact'), 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) - }) + }) return form({ 'factureform': invoice_form, 'action_name': _("Create"), 'articlesformset': articles_formset, 'articles': articles - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change facture to invoice @@ -302,7 +304,7 @@ def facture_pdf(request, facture, **_kwargs): 'email': AssoOption.get_cached_value('contact'), 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) - }) + }) # TODO : change facture to invoice @@ -326,7 +328,7 @@ def edit_facture(request, facture, **_kwargs): fields=('name', 'number'), extra=0, max_num=len(purchases_objects) - ) + ) purchase_form = purchase_form_set( request.POST or None, queryset=purchases_objects @@ -343,7 +345,7 @@ def edit_facture(request, facture, **_kwargs): return form({ 'factureform': invoice_form, 'venteform': purchase_form - }, 'cotisations/edit_facture.html', request) + }, 'cotisations/edit_facture.html', request) # TODO : change facture to invoice @@ -363,7 +365,7 @@ def del_facture(request, facture, **_kwargs): return form({ 'objet': facture, 'objet_name': _("Invoice") - }, 'cotisations/delete.html', request) + }, 'cotisations/delete.html', request) # TODO : change solde to balance @@ -386,7 +388,7 @@ def credit_solde(request, user, **_kwargs): name="solde", prix=invoice.cleaned_data['montant'], number=1 - ) + ) new_purchase.save() messages.success( request, @@ -396,7 +398,7 @@ def credit_solde(request, user, **_kwargs): return form({ 'factureform': invoice, 'action_name': _("Edit") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) @login_required @@ -422,7 +424,7 @@ def add_article(request): return form({ 'factureform': article, 'action_name': _("Add") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) @login_required @@ -443,7 +445,7 @@ def edit_article(request, article_instance, **_kwargs): return form({ 'factureform': article, 'action_name': _('Edit') - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) @login_required @@ -464,7 +466,7 @@ def del_article(request, instances): return form({ 'factureform': article, 'action_name': _("Delete") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change paiement to payment @@ -492,7 +494,7 @@ def add_paiement(request): 'factureform': payment, 'payment_method': payment_method, 'action_name': _("Add") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : chnage paiement to Payment @@ -525,7 +527,7 @@ def edit_paiement(request, paiement_instance, **_kwargs): 'factureform': payment, 'payment_method': payment_method, 'action_name': _("Edit") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change paiement to payment @@ -560,7 +562,7 @@ def del_paiement(request, instances): return form({ 'factureform': payment, 'action_name': _("Delete") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change banque to bank @@ -581,7 +583,7 @@ def add_banque(request): return form({ 'factureform': bank, 'action_name': _("Add") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change banque to bank @@ -603,7 +605,7 @@ def edit_banque(request, banque_instance, **_kwargs): return form({ 'factureform': bank, 'action_name': _("Edit") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : chnage banque to bank @@ -638,7 +640,7 @@ def del_banque(request, instances): return form({ 'factureform': bank, 'action_name': _("Delete") - }, 'cotisations/facture.html', request) + }, 'cotisations/facture.html', request) # TODO : change facture to invoice @@ -679,7 +681,7 @@ def control(request): return render(request, 'cotisations/control.html', { 'facture_list': invoice_list, 'controlform': control_invoices_form - }) + }) @login_required @@ -692,7 +694,7 @@ def index_article(request): article_list = Article.objects.order_by('name') return render(request, 'cotisations/index_article.html', { 'article_list': article_list - }) + }) # TODO : change paiement to payment @@ -705,7 +707,7 @@ def index_paiement(request): payment_list = Paiement.objects.order_by('moyen') return render(request, 'cotisations/index_paiement.html', { 'paiement_list': payment_list - }) + }) # TODO : change banque to bank @@ -718,7 +720,7 @@ def index_banque(request): bank_list = Banque.objects.order_by('name') return render(request, 'cotisations/index_banque.html', { 'banque_list': bank_list - }) + }) @login_required @@ -739,7 +741,7 @@ def index(request): invoice_list = re2o_paginator(request, invoice_list, pagination_number) return render(request, 'cotisations/index.html', { 'facture_list': invoice_list - }) + }) # TODO : merge this function with new_facture() which is nearly the same @@ -789,7 +791,7 @@ def new_facture_solde(request, userid): for art_item in articles: if art_item.cleaned_data: total_price += art_item.cleaned_data['article']\ - .prix*art_item.cleaned_data['quantity'] + .prix*art_item.cleaned_data['quantity'] if float(user.solde) - float(total_price) < negative_balance: messages.error( request, @@ -851,7 +853,7 @@ def new_facture_solde(request, userid): return form({ 'venteform': article_formset, 'articlelist': article_list - }, 'cotisations/new_facture_solde.html', request) + }, 'cotisations/new_facture_solde.html', request) # TODO : change recharge to refill @@ -886,10 +888,10 @@ def recharge(request): ) purchase.save() # content = online_payment.PAYMENT_SYSTEM[ - # AssoOption.get_cached_value('payment') + # AssoOption.get_cached_value('payment') # ](invoice, request) return render(request, 'cotisations/payment.html', content) return form({ 'rechargeform': refill_form, 'solde': request.user.solde - }, 'cotisations/recharge.html', request) + }, 'cotisations/recharge.html', request) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 25b4992e..6acc633a 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -287,7 +287,8 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} {% if user_solde %} - Ajouter une cotisation par solde + Ajouter une cotisation par solde + {% endif %} {% acl_end %}
From 9c69d259d9cb0a54d673b825d733d6fedfe2cdde Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 14:49:13 +0200 Subject: [PATCH 19/38] =?UTF-8?q?Les=20factures=20valid=C3=A9es=20par=20co?= =?UTF-8?q?mnpay=20sont=20effectivement=20valid=C3=A9es.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/payment_methods/comnpay/models.py | 35 ++++++++++- cotisations/payment_methods/comnpay/views.py | 63 +++++-------------- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index 466f6d14..0319ddd7 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -1,11 +1,13 @@ from django.db import models from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import ugettext as _ from cotisations.models import Paiement from cotisations.payment_methods.mixins import PaymentMethodMixin from .aes_field import AESEncryptedField -from .views import comnpay +from .comnpay import Transaction class ComnpayPayment(PaymentMethodMixin, models.Model): @@ -29,7 +31,34 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): ) def end_payment(self, invoice, request): + """ + Build a request to start the negociation with Comnpay by using + a facture id, the price and the secret transaction data stored in + the preferences. + """ invoice.valid = False invoice.save() - content = comnpay(invoice, request, self) - return render(request, 'cotisations/payment.html', content) + host = request.get_host() + p = Transaction( + str(self.payment_credential), + str(self.payment_pass), + 'https://' + host + reverse( + 'cotisations:comnpay:accept_payment', + kwargs={'factureid': invoice.id} + ), + 'https://' + host + reverse('cotisations:comnpay:refuse_payment'), + 'https://' + host + reverse('cotisations:comnpay:ipn'), + "", + "D" + ) + r = { + 'action': 'https://secure.homologation.comnpay.com', + 'method': 'POST', + 'content': p.buildSecretHTML( + _("Pay invoice no : ")+str(invoice.id), + invoice.prix_total(), + idTransaction=str(invoice.id) + ), + 'amount': invoice.prix_total(), + } + return render(request, 'cotisations/payment.html', r) diff --git a/cotisations/payment_methods/comnpay/views.py b/cotisations/payment_methods/comnpay/views.py index 85b237ff..814a51c8 100644 --- a/cotisations/payment_methods/comnpay/views.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -14,9 +14,9 @@ from django.utils.datastructures import MultiValueDictKeyError from django.utils.translation import ugettext as _ from django.http import HttpResponse, HttpResponseBadRequest -from preferences.models import AssoOption from cotisations.models import Facture from .comnpay import Transaction +from .models import ComnpayPayment @csrf_exempt @@ -35,7 +35,8 @@ def accept_payment(request, factureid): ) # In case a cotisation was bought, inform the user, the # cotisation time has been extended too - if any(purchase.type_cotisation for purchase in invoice.vente_set.all()): + if any(purchase.type_cotisation + for purchase in invoice.vente_set.all()): messages.success( request, _("The cotisation of %(member_name)s has been \ @@ -80,28 +81,29 @@ def ipn(request): except MultiValueDictKeyError: return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") - if not p.validSec(data, AssoOption.get_cached_value('payment_pass')): - 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'] - - # Checking that the payment is actually for us. - if not idTpe == AssoOption.get_cached_value('payment_id'): - 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) + payment_method = get_object_or_404( + ComnpayPayment, payment=facture.paiement) - # Checking that the payment is valid + if not p.validSec(data, payment_method.payment_pass): + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + result = True if (request.POST['result'] == 'OK') else False + idTpe = request.POST['idTpe'] + + # Checking that the payment is actually for us. + if not idTpe == payment_method.payment_credential: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + # Checking that the payment is valid if not result: # Payment failed: Cancelling the invoice operation - facture.delete() # And send the response to Comnpay indicating we have well # received the failure information. return HttpResponse("HTTP/1.1 200 OK") @@ -112,36 +114,3 @@ def ipn(request): # Everything worked we send a reponse to Comnpay indicating that # it's ok for them to proceed return HttpResponse("HTTP/1.0 200 OK") - - -def comnpay(facture, request, payment): - """ - Build a request to start the negociation with Comnpay by using - a facture id, the price and the secret transaction data stored in - the preferences. - """ - host = request.get_host() - p = Transaction( - str(payment.payment_credential), - str(payment.payment_pass), - 'https://' + host + reverse( - 'cotisations:comnpay:accept_payment', - kwargs={'factureid': facture.id} - ), - 'https://' + host + reverse('cotisations:comnpay:refuse_payment'), - 'https://' + host + reverse('cotisations:comnpay:ipn'), - "", - "D" - ) - r = { - 'action': 'https://secure.homologation.comnpay.com', - 'method': 'POST', - 'content': p.buildSecretHTML( - "Paiement de la facture "+str(facture.id), - facture.prix_total(), - idTransaction=str(facture.id) - ), - 'amount': facture.prix_total(), - } - return r - From 1f6b30c82fd0d09d72bc243a1962bd161a483c28 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 14:54:41 +0200 Subject: [PATCH 20/38] =?UTF-8?q?M=C3=AAme=20template=20pour=20la=20valida?= =?UTF-8?q?tion=20de=20paiement.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/payment_methods/cheque/views.py | 7 +++- .../templates/cotisations/payment.html | 1 + .../templates/cotisations/payment_form.html | 37 ------------------- 3 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 cotisations/templates/cotisations/payment_form.html diff --git a/cotisations/payment_methods/cheque/views.py b/cotisations/payment_methods/cheque/views.py index cb009deb..bcf707f9 100644 --- a/cotisations/payment_methods/cheque/views.py +++ b/cotisations/payment_methods/cheque/views.py @@ -39,6 +39,9 @@ def cheque(request, invoice_pk): ) return render( request, - 'cotisations/payment_form.html', - {'form': form} + 'cotisations/payment.html', + { + 'form': form, + 'amount': invoice.prix_total() + } ) diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html index bfdde5c4..57940485 100644 --- a/cotisations/templates/cotisations/payment.html +++ b/cotisations/templates/cotisations/payment.html @@ -37,6 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ content | safe }} + {% bootstrap_form form %} {% trans "Pay" as tr_pay %} {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
diff --git a/cotisations/templates/cotisations/payment_form.html b/cotisations/templates/cotisations/payment_form.html deleted file mode 100644 index 1057b7cb..00000000 --- a/cotisations/templates/cotisations/payment_form.html +++ /dev/null @@ -1,37 +0,0 @@ -{% 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 %} -

{% trans form.title %}

-
- {% csrf_token %} - {% bootstrap_form form %} - {% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %} -
-{% endblock %} From f4e7ef974ac3238a9932299cd3f9a37745cbf621 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 16:22:38 +0200 Subject: [PATCH 21/38] =?UTF-8?q?Passage=20du=20paiement=20par=20solde=20e?= =?UTF-8?q?n=20paiement=20personnalis=C3=A9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0032_chequepayment_comnpaypayment.py | 6 +- cotisations/migrations/0033_balancepayment.py | 41 ++++++++++++++ cotisations/models.py | 2 +- cotisations/payment_methods/__init__.py | 3 +- .../payment_methods/balance/.views.py.swo | Bin 0 -> 12288 bytes .../payment_methods/balance/__init__.py | 7 +++ cotisations/payment_methods/balance/models.py | 50 +++++++++++++++++ cotisations/views.py | 27 --------- users/models.py | 53 ++++++++---------- 9 files changed, 128 insertions(+), 61 deletions(-) create mode 100644 cotisations/migrations/0033_balancepayment.py create mode 100644 cotisations/payment_methods/balance/.views.py.swo create mode 100644 cotisations/payment_methods/balance/__init__.py create mode 100644 cotisations/payment_methods/balance/models.py diff --git a/cotisations/migrations/0032_chequepayment_comnpaypayment.py b/cotisations/migrations/0032_chequepayment_comnpaypayment.py index d2c67308..e0717a05 100644 --- a/cotisations/migrations/0032_chequepayment_comnpaypayment.py +++ b/cotisations/migrations/0032_chequepayment_comnpaypayment.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import cotisations.payment_methods.comnpay.aes_field -import cotisations.payment_methods.models +import cotisations.payment_methods.mixins from django.db import migrations, models import django.db.models.deletion @@ -47,7 +47,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), ], - bases=(cotisations.payment_methods.models.PaymentMethodMixin, models.Model), + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), ), migrations.CreateModel( name='ComnpayPayment', @@ -57,7 +57,7 @@ class Migration(migrations.Migration): ('payment_pass', cotisations.payment_methods.comnpay.aes_field.AESEncryptedField(blank=True, max_length=255, null=True)), ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), ], - bases=(cotisations.payment_methods.models.PaymentMethodMixin, models.Model), + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), ), migrations.RunPython(add_comnpay), migrations.RunPython(add_cheque), diff --git a/cotisations/migrations/0033_balancepayment.py b/cotisations/migrations/0033_balancepayment.py new file mode 100644 index 00000000..9308a7b0 --- /dev/null +++ b/cotisations/migrations/0033_balancepayment.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-03 13:53 +from __future__ import unicode_literals + +import cotisations.payment_methods.mixins +from django.db import migrations, models +import django.db.models.deletion + + +def add_solde(apps, schema_editor): + OptionalUser = apps.get_model('preferences', 'OptionalUser') + options, _created = OptionalUser.objects.get_or_create() + + Payment = apps.get_model('cotisations', 'Paiement') + BalancePayment = apps.get_model('cotisations', 'BalancePayment') + + solde, _created = Payment.objects.get_or_create(moyen="solde") + balance = BalancePayment() + balance.payment = solde + balance.minimum_balance = options.solde_negatif + balance.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0032_chequepayment_comnpaypayment'), + ] + + operations = [ + migrations.CreateModel( + name='BalancePayment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('minimum_balance', models.DecimalField(decimal_places=2, help_text='The minimal amount of money allowed for the balance at the end of a payment. You can specify negative amount.', max_digits=5, verbose_name='Minimum balance')), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), + ], + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), + ), + migrations.RunPython(add_solde) + ] diff --git a/cotisations/models.py b/cotisations/models.py index 66e045db..ba285c90 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -668,7 +668,7 @@ class Paiement(RevMixin, AclMixin, models.Model): ) return redirect(reverse( 'users:profil', - kwargs={'userid': request.user.pk} + kwargs={'userid': invoice.user.pk} )) diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py index a85fa49c..422bd069 100644 --- a/cotisations/payment_methods/__init__.py +++ b/cotisations/payment_methods/__init__.py @@ -1,6 +1,7 @@ -from . import comnpay, cheque, urls +from . import comnpay, cheque, balance, urls PAYMENT_METHODS = [ comnpay, cheque, + balance, ] diff --git a/cotisations/payment_methods/balance/.views.py.swo b/cotisations/payment_methods/balance/.views.py.swo new file mode 100644 index 0000000000000000000000000000000000000000..179420c2e997e34b1405e3cab1206174919ee680 GIT binary patch literal 12288 zcmeI2&5ImG7>6s0-^OV4VgxD2!_30iv&rsZT^$GrYZlEKa950iO>KA0Ol79K+N$cA z#2_Mq9+OKzMQ@6bvx<07)T2502Plbp6~9n%Z~m&gdS-jGWY3Xmc(!M|->Q21d8>Ps`&D~xo;Om!xcNk{P`vJ*)o zG&4e`x|6Xp2~SO!a5Ipx?rgFVOC#PH3qH|7c4p76eSkOM4P47WZhDU_?I#OI4i~Tb z!;OdNy$3h0)xzKN2D|}pz#H%eya8{(8}J6af&ZBSlkFp~;hAo5&((3~eLK$XSAX&b zya8{(8}J6a0dK$?@CLjAZ@?Sy2E2jm&;Y~t{FiPcbtB?z>qHgWCg+3Ba|F1x0Jfk{TqS+wFW=Ukieg5oAuDOEg; zxoS=OsgIg#9@e*DbywI(^J%zEq}HvyU=N#TAX07YcCGCx6rFd}?Nz;;DcKYC%D7_0Lh;BT zt1;sEOhSFsu+=yza~iQUm4>1~)C`2KCJ~(oGoXw{16%K)QEz)ywGys1>vJ7&rKFn) z5LnH~QZCHcp@go?wCdBMO`j-6J5lb}bH3{_FyH5f8KYWN=T+s~v{{dBwu?%Xsk`X1 zeJD3iK~X4GxH!MqS}kkYrrEG6R+tUvNX&aqKviN})#4?y_m|1hk?e~!v^wA%f9#N+ zk_ip$)Pl++lgezLDeiV6V?LVRuRXNuVcUxs)T38egfer=bk(tI5}#seUk14mBOMsU zQavj6KUQhv@MidFV_Sx0BCbg!Q=`OY6&D_XBeZRlLhT)8<-NTbmp$GG(XP$!Y{m};4DTI}k?rC3DF;G&VP yhIWpZOGMG!&9dU1p3UwZubN+5vGV%06*JYZt+id$er>gT%c~{RudTbUt^WbL5Xuk$ literal 0 HcmV?d00001 diff --git a/cotisations/payment_methods/balance/__init__.py b/cotisations/payment_methods/balance/__init__.py new file mode 100644 index 00000000..adfca187 --- /dev/null +++ b/cotisations/payment_methods/balance/__init__.py @@ -0,0 +1,7 @@ +""" +This module contains a method to pay online using user balance. +""" +from . import models +NAME = "BALANCE" + +PaymentMethod = models.BalancePayment diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py new file mode 100644 index 00000000..b2ff96cc --- /dev/null +++ b/cotisations/payment_methods/balance/models.py @@ -0,0 +1,50 @@ +from django.db import models +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _l +from django.contrib import messages + + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + + +class BalancePayment(PaymentMethodMixin, models.Model): + """ + The model allowing you to pay with a cheque. + """ + payment = models.OneToOneField( + Paiement, + related_name='payment_method', + editable=False + ) + minimum_balance = models.DecimalField( + verbose_name=_l("Minimum balance"), + help_text=_l("The minimal amount of money allowed for the balance" + " at the end of a payment. You can specify negative " + "amount." + ), + max_digits=5, + decimal_places=2, + ) + + def end_payment(self, invoice, request): + user = invoice.user + total_price = invoice.prix_total() + if float(user.solde) - float(total_price) < self.minimum_balance: + invoice.valid = False + invoice.save() + messages.error( + request, + _("Your balance is too low for this operation.") + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': user.id} + )) + return invoice.paiement.end_payment( + invoice, + request, + use_payment_method=False + ) diff --git a/cotisations/views.py b/cotisations/views.py index 0acd6b78..fbf8d7ab 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -139,33 +139,6 @@ def new_facture(request, user, userid): articles = article_formset # Check if at leat one article has been selected if any(art.cleaned_data for art in articles): - user_balance = OptionalUser.get_cached_value('user_solde') - negative_balance = OptionalUser.get_cached_value('solde_negatif') - # If the paiement using balance has been activated, - # checking that the total price won't get the user under - # the authorized minimum (negative_balance) - if user_balance: - # TODO : change Paiement to Payment - if new_invoice_instance.paiement == ( - Paiement.objects.get_or_create(moyen='solde')[0] - ): - total_price = 0 - for art_item in articles: - if art_item.cleaned_data: - total_price += ( - art_item.cleaned_data['article'].prix * - art_item.cleaned_data['quantity'] - ) - if (float(user.solde) - float(total_price) - < negative_balance): - messages.error( - request, - _("Your balance is too low for this operation.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) new_invoice_instance.save() # Building a purchase for each article sold diff --git a/users/models.py b/users/models.py index d0e21997..50f7c51f 100644 --- a/users/models.py +++ b/users/models.py @@ -426,36 +426,31 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, @cached_property def solde(self): - """ Renvoie le solde d'un user. Vérifie que l'option solde est - activé, retourne 0 sinon. + """ Renvoie le solde d'un user. Somme les crédits de solde et retire les débit payés par solde""" - user_solde = OptionalUser.get_cached_value('user_solde') - if user_solde: - solde_objects = Paiement.objects.filter(moyen='Solde') - somme_debit = Vente.objects.filter( - facture__in=Facture.objects.filter( - user=self, - paiement__in=solde_objects, - valid=True - ) - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] or 0 - somme_credit = Vente.objects.filter( - facture__in=Facture.objects.filter(user=self, valid=True), - name="solde" - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] or 0 - return somme_credit - somme_debit - else: - return 0 + solde_objects = Paiement.objects.filter(moyen='solde') + somme_debit = Vente.objects.filter( + facture__in=Facture.objects.filter( + user=self, + paiement__in=solde_objects, + valid=True + ) + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + somme_credit = Vente.objects.filter( + facture__in=Facture.objects.filter(user=self, valid=True), + name="solde" + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + return somme_credit - somme_debit def user_interfaces(self, active=True): """ Renvoie toutes les interfaces dont les machines appartiennent à From 27bc7301ab2410a7c52cd04e838c8b088ab9caaf Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 16:31:06 +0200 Subject: [PATCH 22/38] Nommage des champs ComNpay --- .../migrations/0034_auto_20180703_0929.py | 26 +++++++++++++++++++ cotisations/payment_methods/comnpay/models.py | 5 +++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 cotisations/migrations/0034_auto_20180703_0929.py diff --git a/cotisations/migrations/0034_auto_20180703_0929.py b/cotisations/migrations/0034_auto_20180703_0929.py new file mode 100644 index 00000000..1e726cd1 --- /dev/null +++ b/cotisations/migrations/0034_auto_20180703_0929.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-03 14:29 +from __future__ import unicode_literals + +import cotisations.payment_methods.comnpay.aes_field +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0033_balancepayment'), + ] + + operations = [ + migrations.AlterField( + model_name='comnpaypayment', + name='payment_credential', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='ComNpay VAD Number'), + ), + migrations.AlterField( + model_name='comnpaypayment', + name='payment_pass', + field=cotisations.payment_methods.comnpay.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay Secret Key'), + ), + ] diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index 0319ddd7..fefa3587 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -2,6 +2,7 @@ from django.db import models from django.shortcuts import render from django.urls import reverse from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _l from cotisations.models import Paiement from cotisations.payment_methods.mixins import PaymentMethodMixin @@ -22,12 +23,14 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): payment_credential = models.CharField( max_length=255, default='', - blank=True + blank=True, + verbose_name=_l("ComNpay VAD Number"), ) payment_pass = AESEncryptedField( max_length=255, null=True, blank=True, + verbose_name=_l("ComNpay Secret Key"), ) def end_payment(self, invoice, request): From 5f1e2380c8dd682fa04d75c76f2384104db79894 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 16:46:29 +0200 Subject: [PATCH 23/38] =?UTF-8?q?Emp=C3=AAche=20le=20changement=20de=20m?= =?UTF-8?q?=C3=A9thode=20de=20paiement=20apr=C3=A8s=20cr=C3=A9ation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/payment_methods/forms.py | 11 +++++++++-- cotisations/views.py | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py index f5bf4f76..37653c3a 100644 --- a/cotisations/payment_methods/forms.py +++ b/cotisations/payment_methods/forms.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _l from . import PAYMENT_METHODS from cotisations.utils import find_payment_method -def payment_method_factory(payment, *args, **kwargs): +def payment_method_factory(payment, *args, creation=True, **kwargs): payment_method = kwargs.pop('instance', find_payment_method(payment)) if payment_method is not None: return forms.modelform_factory(type(payment_method), fields='__all__')( @@ -13,7 +13,10 @@ def payment_method_factory(payment, *args, **kwargs): instance=payment_method, **kwargs ) - return PaymentMethodForm(payment_method, *args, **kwargs) + elif creation: + return PaymentMethodForm(payment_method, *args, **kwargs) + else: + return forms.Form() class PaymentMethodForm(forms.Form): @@ -25,6 +28,10 @@ class PaymentMethodForm(forms.Form): payment_method = forms.ChoiceField( label=_l("Special payment method"), + help_text=_l("Warning : You will not be able to change the payment " + "method later. But you will be allowed to edit its " + "options." + ), required=False ) diff --git a/cotisations/views.py b/cotisations/views.py index fbf8d7ab..bee266d0 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -485,7 +485,8 @@ def edit_paiement(request, paiement_instance, **_kwargs): payment_method = payment_method_factory( paiement_instance, request.POST or None, - prefix='payment_method' + prefix='payment_method', + creation=False ) if payment.is_valid() and payment_method.is_valid(): From 5887eb68ae6d9ba3706e87f5aa37e2c88ce9ba50 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 18:53:44 +0200 Subject: [PATCH 24/38] ACL sur les paiements. --- CHANGELOG.md | 11 ++++ cotisations/forms.py | 40 ++++++-------- .../migrations/0035_auto_20180703_1005.py | 28 ++++++++++ .../migrations/0036_auto_20180703_1056.py | 28 ++++++++++ cotisations/models.py | 54 ++++++++++++++++--- cotisations/views.py | 39 +++++--------- 6 files changed, 141 insertions(+), 59 deletions(-) create mode 100644 cotisations/migrations/0035_auto_20180703_1005.py create mode 100644 cotisations/migrations/0036_auto_20180703_1056.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b9b8ca..d846ad09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,3 +94,14 @@ If you to restrict the IP which can see the debug, use the `INTERNAL_IPS` option ``` INTERNAL_IPS = ["10.0.0.1", "10.0.0.2"] ``` + +## MR 174 : Fix online payment + allow users to pay their subscription + +Add the possibility to use custom payment methods. There is also a boolean field on the +Payments allowing every user to use some kinds of payment. You have to add the rights `cotisations.use_every_payment` and `cotisations.buy_every_article` +to the staff members so they can use every type of payment to buy anything. + +Don't forget to run migrations, several settings previously in the `preferences` app ar now +in their own Payment models. + +To have a closer look on how the payments works, pleas go to the wiki. diff --git a/cotisations/forms.py b/cotisations/forms.py index 0ee4c143..9d638221 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -53,16 +53,16 @@ class NewFactureForm(FormRevMixin, ModelForm): Form used to create a new invoice by using a payment method, a bank and a cheque number. """ + def __init__(self, *args, **kwargs): + user = kwargs.pop('user') prefix = kwargs.pop('prefix', self.Meta.model.__name__) - allowed_payment = kwargs.pop('allowed_payment', None) super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) - # TODO : remove the use of cheque and banque and paiement - # for something more generic or at least in English - if allowed_payment: - self.fields['paiement'].queryset = allowed_payment self.fields['paiement'].empty_label = \ _("Select a payment method") + self.fields['paiement'].queryset = Paiement.objects.filter( + pk__in=map(lambda x: x.pk, Paiement.find_allowed_payments(user)) + ) class Meta: model = Facture @@ -71,16 +71,10 @@ class NewFactureForm(FormRevMixin, ModelForm): def clean(self): cleaned_data = super(NewFactureForm, self).clean() paiement = cleaned_data.get('paiement') - cheque = cleaned_data.get('cheque') - banque = cleaned_data.get('banque') if not paiement: raise forms.ValidationError( _("A payment method must be specified.") ) - elif paiement.type_paiement == 'check' and not (cheque and banque): - raise forms.ValidationError( - _("A cheque number and a bank must be specified.") - ) return cleaned_data @@ -103,8 +97,7 @@ class CreditSoldeForm(NewFactureForm): montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) -class SelectUserArticleForm( - FormRevMixin, Form): +class SelectUserArticleForm(FormRevMixin, Form): """ Form used to select an article during the creation of an invoice for a member. @@ -123,12 +116,11 @@ class SelectUserArticleForm( ) def __init__(self, *args, **kwargs): - self_subscription = kwargs.pop('is_self_subscription', False) + user = kwargs.pop('user') super(SelectUserArticleForm, self).__init__(*args, **kwargs) - if self_subscription: - self.fields['article'].queryset = Article.objects.filter( - Q(type_user='All') | Q(type_user='Adherent') - ).filter(allow_self_subscription=True) + self.fields['article'].queryset = Article.objects.filter( + pk__in=map(lambda x: x.pk, Article.find_allowed_articles(user)) + ) class SelectClubArticleForm(Form): @@ -150,12 +142,11 @@ class SelectClubArticleForm(Form): ) def __init__(self, *args, **kwargs): - self_subscription = kwargs.pop('is_self_subscription', False) + user = kwargs.pop('user') super(SelectClubArticleForm, self).__init__(*args, **kwargs) - if self_subscription: - self.fields['article'].queryset = Article.objects.filter( - Q(type_user='All') | Q(type_user='Club') - ).filter(allow_self_subscription=True) + self.fields['article'].queryset = Article.objects.filter( + pk__in=map(lambda x: x.pk, Article.find_allowed_articles(user)) + ) # TODO : change Facture to Invoice @@ -242,7 +233,7 @@ class PaiementForm(FormRevMixin, ModelForm): class Meta: model = Paiement # TODO : change moyen to method and type_paiement to payment_type - fields = ['moyen', 'allow_self_subscription'] + fields = ['moyen', 'available_for_everyone'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) @@ -315,6 +306,7 @@ class NewFactureSoldeForm(NewFactureForm): """ Form used to create an invoice """ + def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewFactureSoldeForm, self).__init__( diff --git a/cotisations/migrations/0035_auto_20180703_1005.py b/cotisations/migrations/0035_auto_20180703_1005.py new file mode 100644 index 00000000..3b741d27 --- /dev/null +++ b/cotisations/migrations/0035_auto_20180703_1005.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-03 15:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0034_auto_20180703_0929'), + ] + + operations = [ + migrations.AlterModelOptions( + name='paiement', + options={'permissions': (('view_paiement', "Can see a payement's details"), ('use', 'Can use a payement')), 'verbose_name': 'Payment method', 'verbose_name_plural': 'Payment methods'}, + ), + migrations.RemoveField( + model_name='paiement', + name='allow_self_subscription', + ), + migrations.AddField( + model_name='paiement', + name='available_for_everyone', + field=models.BooleanField(default=False, verbose_name='Is available for every user'), + ), + ] diff --git a/cotisations/migrations/0036_auto_20180703_1056.py b/cotisations/migrations/0036_auto_20180703_1056.py new file mode 100644 index 00000000..d14f20ad --- /dev/null +++ b/cotisations/migrations/0036_auto_20180703_1056.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-03 15:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0035_auto_20180703_1005'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'permissions': (('view_article', "Can see an article's details"), ('buy_every_article', 'Can buy every_article')), 'verbose_name': 'Article', 'verbose_name_plural': 'Articles'}, + ), + migrations.RemoveField( + model_name='article', + name='allow_self_subscription', + ), + migrations.AddField( + model_name='article', + name='available_for_everyone', + field=models.BooleanField(default=False, verbose_name='Is available for every user'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index ba285c90..58e0f2d3 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -49,7 +49,6 @@ from django.contrib import messages from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin -from preferences.models import OptionalUser from cotisations.utils import find_payment_method @@ -227,10 +226,11 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): :return: a message and a boolean which is True if the user can create an invoice or if the `options.allow_self_subscription` is set. """ - if OptionalUser.get_cached_value('allow_self_subscription'): - return True, None + nb_payments = len(Paiement.find_allowed_payments(user_request)) + nb_articles = len(Article.find_allowed_articles(user_request)) return ( - user_request.has_perm('cotisations.add_facture'), + user_request.has_perm('cotisations.add_facture') + or (nb_payments*nb_articles), _("You don't have the right to create an invoice.") ) @@ -522,9 +522,9 @@ class Article(RevMixin, AclMixin, models.Model): max_length=255, verbose_name=_l("Type of cotisation") ) - allow_self_subscription = models.BooleanField( + available_for_everyone = models.BooleanField( default=False, - verbose_name=_l("Is available for self subscription") + verbose_name=_l("Is available for every user") ) unique_together = ('name', 'type_user') @@ -532,6 +532,7 @@ class Article(RevMixin, AclMixin, models.Model): class Meta: permissions = ( ('view_article', _l("Can see an article's details")), + ('buy_every_article', _l("Can buy every_article")) ) verbose_name = "Article" verbose_name_plural = "Articles" @@ -549,6 +550,24 @@ class Article(RevMixin, AclMixin, models.Model): def __str__(self): return self.name + def can_buy_article(self, user, *_args, **_kwargs): + """Check if a user can buy this article. + + :param self: The article + :param user: The user requesting buying + :returns: A boolean stating if usage is granted and an explanation + message if the boolean is `False`. + """ + return ( + self.available_for_everyone + or user.has_perm('cotisations.buy_every_article'), + _("You cannot use this Payment.") + ) + + @classmethod + def find_allowed_articles(cls, user): + return [p for p in cls.objects.all() if p.can_buy_article(user)[0]] + class Banque(RevMixin, AclMixin, models.Model): """ @@ -601,14 +620,15 @@ class Paiement(RevMixin, AclMixin, models.Model): default=0, verbose_name=_l("Payment type") ) - allow_self_subscription = models.BooleanField( + available_for_everyone = models.BooleanField( default=False, - verbose_name=_l("Is available for self subscription") + verbose_name=_l("Is available for every user") ) class Meta: permissions = ( ('view_paiement', _l("Can see a payement's details")), + ('use_every_payment', _l("Can use every payement")), ) verbose_name = _l("Payment method") verbose_name_plural = _l("Payment methods") @@ -671,6 +691,24 @@ class Paiement(RevMixin, AclMixin, models.Model): kwargs={'userid': invoice.user.pk} )) + def can_use_payment(self, user, *_args, **_kwargs): + """Check if a user can use this payment. + + :param self: The payment + :param user: The user requesting usage + :returns: A boolean stating if usage is granted and an explanation + message if the boolean is `False`. + """ + return ( + self.available_for_everyone + or user.has_perm('cotisations.use_every_payment'), + _("You cannot use this Payment.") + ) + + @classmethod + def find_allowed_payments(cls, user): + return [p for p in cls.objects.all() if p.can_use_payment(user)[0]] + class Cotisation(RevMixin, AclMixin, models.Model): """ diff --git a/cotisations/views.py b/cotisations/views.py index bee266d0..40b09303 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -98,40 +98,22 @@ def new_facture(request, user, userid): article_list = Article.objects.filter( Q(type_user='All') | Q(type_user=request.user.class_name) ) - # Building the invocie form and the article formset - is_self_subscription = False - can_create_invoice = request.user.has_perm('cotisations.add_facture') - allow_self_subscription = OptionalUser.get_cached_value( - 'allow_self_subscription') - if not can_create_invoice: - if allow_self_subscription: - is_self_subscription = True - article_list = article_list.filter(allow_self_subscription=True) - allowed_payment = Paiement.objects.filter( - allow_self_subscription=True) - invoice_form = NewFactureForm( - request.POST or None, instance=invoice, allowed_payment=allowed_payment) - else: - messages.error( - request, - _("You cannot subscribe. Please ask to the staff.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - else: - invoice_form = NewFactureForm(request.POST or None, instance=invoice) + # Building the invoice form and the article formset + invoice_form = NewFactureForm( + request.POST or None, + instance=invoice, + user=request.user + ) if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)( request.POST or None, - form_kwargs={'is_self_subscription': is_self_subscription} + form_kwargs={'user': request.user} ) else: article_formset = formset_factory(SelectUserArticleForm)( request.POST or None, - form_kwargs={'is_self_subscription': is_self_subscription} + form_kwargs={'user': request.user} ) if invoice_form.is_valid() and article_formset.is_valid(): @@ -156,7 +138,10 @@ def new_facture(request, user, userid): ) new_purchase.save() - return new_invoice_instance.paiement.end_payment(new_invoice_instance, request) + return new_invoice_instance.paiement.end_payment( + new_invoice_instance, + request + ) messages.error( request, From dbe582238fb52b465219d67c4b24e68a37b21eb7 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 19:30:31 +0200 Subject: [PATCH 25/38] =?UTF-8?q?Ajout=20de=20la=20possibilit=C3=A9=20de?= =?UTF-8?q?=20recharger=20son=20solde.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/forms.py | 29 ++++--- .../migrations/0037_auto_20180703_1202.py | 35 +++++++++ cotisations/payment_methods/balance/models.py | 7 ++ .../templates/cotisations/payment.html | 2 + cotisations/views.py | 21 +----- users/templates/users/profil.html | 2 +- users/views.py | 75 ++++++++++--------- 7 files changed, 105 insertions(+), 66 deletions(-) create mode 100644 cotisations/migrations/0037_auto_20180703_1202.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 9d638221..b59eb6c3 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -42,10 +42,10 @@ from django.core.validators import MinValueValidator from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l -from preferences.models import OptionalUser from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin from .models import Article, Paiement, Facture, Banque +from .payment_methods import balance class NewFactureForm(FormRevMixin, ModelForm): @@ -362,34 +362,41 @@ class RechargeForm(FormRevMixin, Form): min_value=0.01, validators=[] ) + payment = forms.ModelChoiceField( + queryset=Paiement.objects.none(), + label=_l("Payment method") + ) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') super(RechargeForm, self).__init__(*args, **kwargs) + self.fields['payment'].empty_label = \ + _("Select a payment method") + self.fields['payment'].queryset = Paiement.objects.filter( + pk__in=map(lambda x: x.pk, + Paiement.find_allowed_payments(self.user)) + ) def clean_value(self): """ - Returns a cleaned vlaue from the received form by validating + Returns a cleaned value from the received form by validating the value is well inside the possible limits """ value = self.cleaned_data['value'] - if value < OptionalUser.get_cached_value('min_online_payment'): + balance_method, _created = balance.PaymentMethod\ + .objects.get_or_create() + if value < balance_method.minimum_balance: raise forms.ValidationError( _("Requested amount is too small. Minimum amount possible : \ %(min_online_amount)s €.") % { - 'min_online_amount': OptionalUser.get_cached_value( - 'min_online_payment' - ) + 'min_online_amount': balance_method.minimum_balance } ) - if value + self.user.solde > \ - OptionalUser.get_cached_value('max_solde'): + if value + self.user.solde > balance_method.maximum_balance: raise forms.ValidationError( _("Requested amount is too high. Your balance can't exceed \ %(max_online_balance)s €.") % { - 'max_online_balance': OptionalUser.get_cached_value( - 'max_solde' - ) + 'max_online_balance': balance_method.maximum_balance } ) return value diff --git a/cotisations/migrations/0037_auto_20180703_1202.py b/cotisations/migrations/0037_auto_20180703_1202.py new file mode 100644 index 00000000..3860ef87 --- /dev/null +++ b/cotisations/migrations/0037_auto_20180703_1202.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-03 17:02 +from __future__ import unicode_literals + +from django.db import migrations, models + +def create_max_balance(apps, schema_editor): + OptionalUser = apps.get_model('preferences', 'OptionalUser') + Payment = apps.get_model('cotisations', 'Paiement') + + balance, _created = Payment.objects.get_or_create(moyen='solde') + options, _created = OptionalUser.objects.get_or_create() + + balance.maximum_balance = options.max_solde + balance.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0036_auto_20180703_1056'), + ] + + operations = [ + migrations.AlterModelOptions( + name='paiement', + options={'permissions': (('view_paiement', "Can see a payement's details"), ('use_every_payment', 'Can use every payement')), 'verbose_name': 'Payment method', 'verbose_name_plural': 'Payment methods'}, + ), + migrations.AddField( + model_name='balancepayment', + name='maximum_balance', + field=models.DecimalField(decimal_places=2, default=50, help_text='The maximal amount of money allowed for the balance.', max_digits=5, verbose_name='Maximum balance'), + ), + migrations.RunPython(create_max_balance) + ] diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py index b2ff96cc..957244d7 100644 --- a/cotisations/payment_methods/balance/models.py +++ b/cotisations/payment_methods/balance/models.py @@ -28,6 +28,13 @@ class BalancePayment(PaymentMethodMixin, models.Model): max_digits=5, decimal_places=2, ) + maximum_balance = models.DecimalField( + verbose_name=_l("Maximum balance"), + help_text=_l("The maximal amount of money allowed for the balance."), + max_digits=5, + decimal_places=2, + default=50 + ) def end_payment(self, invoice, request): user = invoice.user diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html index 57940485..577ca64f 100644 --- a/cotisations/templates/cotisations/payment.html +++ b/cotisations/templates/cotisations/payment.html @@ -37,7 +37,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ content | safe }} + {% if form %} {% bootstrap_form form %} + {% endif %} {% trans "Pay" as tr_pay %} {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
diff --git a/cotisations/views.py b/cotisations/views.py index 40b09303..dc906686 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -821,24 +821,11 @@ def recharge(request): """ View used to refill the balance by using online payment. """ - if AssoOption.get_cached_value('payment') == 'NONE': - messages.error( - request, - _("Online payment is disabled.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': request.user.id} - )) refill_form = RechargeForm(request.POST or None, user=request.user) if refill_form.is_valid(): invoice = Facture(user=request.user) - payment, _created = Paiement.objects.get_or_create( - moyen='Rechargement en ligne' - ) - invoice.paiement = payment + invoice.paiement = refill_form.cleaned_data['payment'] invoice.valid = False - invoice.save() purchase = Vente.objects.create( facture=invoice, name='solde', @@ -846,10 +833,8 @@ def recharge(request): number=1 ) purchase.save() - # content = online_payment.PAYMENT_SYSTEM[ - # AssoOption.get_cached_value('payment') - # ](invoice, request) - return render(request, 'cotisations/payment.html', content) + invoice.save() + return invoice.paiement.end_payment(invoice, request) return form({ 'rechargeform': refill_form, 'solde': request.user.solde diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 6acc633a..72491b68 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -166,7 +166,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Solde {{ users.solde }} € - {% if user_solde and allow_online_payment %} + {% if user_solde %} Recharger diff --git a/users/views.py b/users/views.py index a125124c..8911c85e 100644 --- a/users/views.py +++ b/users/views.py @@ -49,9 +49,9 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer from reversion import revisions as reversion -from cotisations.models import Facture +from cotisations.models import Facture, Paiement from machines.models import Machine -from preferences.models import OptionalUser, GeneralOption, AssoOption +from preferences.models import GeneralOption from re2o.views import form from re2o.utils import ( all_has_access, @@ -246,7 +246,8 @@ def state(request, user, userid): @can_edit(User, 'groups') def groups(request, user, userid): """ View to edit the groups of a user """ - group_form = GroupForm(request.POST or None, instance=user, user=request.user) + group_form = GroupForm(request.POST or None, + instance=user, user=request.user) if group_form.is_valid(): if group_form.changed_data: group_form.save() @@ -404,23 +405,23 @@ def edit_ban(request, ban_instance, **_kwargs): request ) + @login_required @can_delete(Ban) def del_ban(request, ban, **_kwargs): - """ Supprime un banissement""" - if request.method == "POST": - ban.delete() - messages.success(request, "Le banissement a été supprimé") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(ban.user.id)} - )) - return form( - {'objet': ban, 'objet_name': 'ban'}, - 'users/delete.html', - request - ) - + """ Supprime un banissement""" + if request.method == "POST": + ban.delete() + messages.success(request, "Le banissement a été supprimé") + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(ban.user.id)} + )) + return form( + {'objet': ban, 'objet_name': 'ban'}, + 'users/delete.html', + request + ) @login_required @@ -481,19 +482,20 @@ def edit_whitelist(request, whitelist_instance, **_kwargs): @login_required @can_delete(Whitelist) def del_whitelist(request, whitelist, **_kwargs): - """ Supprime un acces gracieux""" - if request.method == "POST": - whitelist.delete() - messages.success(request, "L'accés gracieux a été supprimé") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(whitelist.user.id)} - )) - return form( - {'objet': whitelist, 'objet_name': 'whitelist'}, - 'users/delete.html', - request - ) + """ Supprime un acces gracieux""" + if request.method == "POST": + whitelist.delete() + messages.success(request, "L'accés gracieux a été supprimé") + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(whitelist.user.id)} + )) + return form( + {'objet': whitelist, 'objet_name': 'whitelist'}, + 'users/delete.html', + request + ) + @login_required @can_create(School) @@ -814,7 +816,7 @@ def index_listright(request): 'users/index_listright.html', { 'listright_list': listright_list, - 'superuser_right' : superuser_right, + 'superuser_right': superuser_right, } ) @@ -837,7 +839,7 @@ def mon_profil(request): return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} - )) + )) @login_required @@ -881,20 +883,20 @@ def profil(request, users, **_kwargs): request.GET.get('order'), SortTable.USERS_INDEX_WHITE ) - user_solde = OptionalUser.get_cached_value('user_solde') - allow_online_payment = True# TODO : AssoOption.get_cached_value('payment') != 'NONE' + balance, _created = Paiement.objects.get_or_create(moyen="solde") + user_solde = Facture.can_create(request.user) \ + and balance.can_use_payment(request.user) return render( request, 'users/profil.html', { 'users': users, 'machines_list': machines, - 'nb_machines' : nb_machines, + 'nb_machines': nb_machines, 'facture_list': factures, 'ban_list': bans, 'white_list': whitelists, 'user_solde': user_solde, - 'allow_online_payment': allow_online_payment, } ) @@ -959,6 +961,7 @@ def process_passwd(request, req): class JSONResponse(HttpResponse): """ Framework Rest """ + def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' From 068a78a5aea3e7dfc1775dd610205406c655d294 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 19:42:43 +0200 Subject: [PATCH 26/38] Fix les erreurs d'affichage du solde sur la page de profil. --- users/templates/users/profil.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 72491b68..00390fdd 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -33,13 +33,11 @@ un {{ users.class_name | lower}}{% else %}active{% else %}désactivée{% endif %}.

{% if user_solde %} -

Votre solde est de {{ user.solde }}€. -{% if allow_online_payment %} +

Votre solde est de {{ users.solde }}€. Recharger -{% endif %}

{% endif %} From a0c696edce60e730662707b67689b6e2ed4da70d Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 21:19:33 +0200 Subject: [PATCH 27/38] =?UTF-8?q?Fix=20le=20rechargement=20:=20la=20factur?= =?UTF-8?q?e=20est=20sauvegard=C3=A9e=20avant=20la=20vente.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cotisations/views.py b/cotisations/views.py index dc906686..79d989e5 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -826,14 +826,13 @@ def recharge(request): invoice = Facture(user=request.user) invoice.paiement = refill_form.cleaned_data['payment'] invoice.valid = False - purchase = Vente.objects.create( + invoice.save() + Vente.objects.create( facture=invoice, name='solde', prix=refill_form.cleaned_data['value'], number=1 ) - purchase.save() - invoice.save() return invoice.paiement.end_payment(invoice, request) return form({ 'rechargeform': refill_form, From fc3ba8af7d0e3d4a985c18a6e3f40d5428b6f438 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 3 Jul 2018 21:38:10 +0200 Subject: [PATCH 28/38] Supprime la vue 'recharge' qui faisait doublon avec 'credit_solde' --- .../templates/cotisations/facture.html | 7 ++- cotisations/urls.py | 5 -- cotisations/views.py | 50 ++++--------------- users/templates/users/profil.html | 4 +- 4 files changed, 19 insertions(+), 47 deletions(-) diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 082759c4..03a4225b 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -31,7 +31,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %} {% bootstrap_form_errors factureform %} - +{% if title %} +

{{title}}

+{% endif %} +{% if balance %} +

{% trans "Current balance :" %}{{balance}}€

+{% endif %}
{% csrf_token %} {% if articlesformset %} diff --git a/cotisations/urls.py b/cotisations/urls.py index 9e318b53..ce6aa0f4 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -138,11 +138,6 @@ urlpatterns = [ views.new_facture_solde, name='new_facture_solde' ), - url( - r'^recharge/$', - views.recharge, - name='recharge' - ), url(r'^$', views.index, name='index'), ] + payment_methods.urls.urlpatterns diff --git a/cotisations/views.py b/cotisations/views.py index 79d989e5..bb391d13 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -326,39 +326,6 @@ def del_facture(request, facture, **_kwargs): }, 'cotisations/delete.html', request) -# TODO : change solde to balance -@login_required -@can_create(Facture) -@can_edit(User) -def credit_solde(request, user, **_kwargs): - """ - View used to edit the balance of a user. - Can be use either to increase or decrease a user's balance. - """ - # TODO : change facture to invoice - invoice = CreditSoldeForm(request.POST or None) - if invoice.is_valid(): - invoice_instance = invoice.save(commit=False) - invoice_instance.user = user - invoice_instance.save() - new_purchase = Vente.objects.create( - facture=invoice_instance, - name="solde", - prix=invoice.cleaned_data['montant'], - number=1 - ) - new_purchase.save() - messages.success( - request, - _("Balance successfully updated.") - ) - return redirect(reverse('cotisations:index')) - return form({ - 'factureform': invoice, - 'action_name': _("Edit") - }, 'cotisations/facture.html', request) - - @login_required @can_create(Article) def add_article(request): @@ -815,11 +782,14 @@ def new_facture_solde(request, userid): }, 'cotisations/new_facture_solde.html', request) -# TODO : change recharge to refill +# TODO : change solde to balance @login_required -def recharge(request): +@can_create(Facture) +@can_edit(User) +def credit_solde(request, user, **_kwargs): """ - View used to refill the balance by using online payment. + View used to edit the balance of a user. + Can be use either to increase or decrease a user's balance. """ refill_form = RechargeForm(request.POST or None, user=request.user) if refill_form.is_valid(): @@ -835,6 +805,8 @@ def recharge(request): ) return invoice.paiement.end_payment(invoice, request) return form({ - 'rechargeform': refill_form, - 'solde': request.user.solde - }, 'cotisations/recharge.html', request) + 'factureform': refill_form, + 'balance': request.user.solde, + 'title': _("Refill your balance"), + 'action_name': _("Pay") + }, 'cotisations/facture.html', request) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 00390fdd..4b50cc99 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -34,7 +34,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} active{% else %}désactivée{% endif %}.

{% if user_solde %}

Votre solde est de {{ users.solde }}€. - + Recharger @@ -165,7 +165,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %} Solde {{ users.solde }} € {% if user_solde %} - + Recharger From b64823f274a3c461d8c8dad419396571a0224b80 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Wed, 4 Jul 2018 17:53:09 +0200 Subject: [PATCH 29/38] =?UTF-8?q?Il=20faut=20apprendre=20=C3=A0=20utiliser?= =?UTF-8?q?=20des=20.gitignore=20un=20jour.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment_methods/balance/.views.py.swo | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cotisations/payment_methods/balance/.views.py.swo diff --git a/cotisations/payment_methods/balance/.views.py.swo b/cotisations/payment_methods/balance/.views.py.swo deleted file mode 100644 index 179420c2e997e34b1405e3cab1206174919ee680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2&5ImG7>6s0-^OV4VgxD2!_30iv&rsZT^$GrYZlEKa950iO>KA0Ol79K+N$cA z#2_Mq9+OKzMQ@6bvx<07)T2502Plbp6~9n%Z~m&gdS-jGWY3Xmc(!M|->Q21d8>Ps`&D~xo;Om!xcNk{P`vJ*)o zG&4e`x|6Xp2~SO!a5Ipx?rgFVOC#PH3qH|7c4p76eSkOM4P47WZhDU_?I#OI4i~Tb z!;OdNy$3h0)xzKN2D|}pz#H%eya8{(8}J6af&ZBSlkFp~;hAo5&((3~eLK$XSAX&b zya8{(8}J6a0dK$?@CLjAZ@?Sy2E2jm&;Y~t{FiPcbtB?z>qHgWCg+3Ba|F1x0Jfk{TqS+wFW=Ukieg5oAuDOEg; zxoS=OsgIg#9@e*DbywI(^J%zEq}HvyU=N#TAX07YcCGCx6rFd}?Nz;;DcKYC%D7_0Lh;BT zt1;sEOhSFsu+=yza~iQUm4>1~)C`2KCJ~(oGoXw{16%K)QEz)ywGys1>vJ7&rKFn) z5LnH~QZCHcp@go?wCdBMO`j-6J5lb}bH3{_FyH5f8KYWN=T+s~v{{dBwu?%Xsk`X1 zeJD3iK~X4GxH!MqS}kkYrrEG6R+tUvNX&aqKviN})#4?y_m|1hk?e~!v^wA%f9#N+ zk_ip$)Pl++lgezLDeiV6V?LVRuRXNuVcUxs)T38egfer=bk(tI5}#seUk14mBOMsU zQavj6KUQhv@MidFV_Sx0BCbg!Q=`OY6&D_XBeZRlLhT)8<-NTbmp$GG(XP$!Y{m};4DTI}k?rC3DF;G&VP yhIWpZOGMG!&9dU1p3UwZubN+5vGV%06*JYZt+id$er>gT%c~{RudTbUt^WbL5Xuk$ From 9005161f338ee81f135bd93d568cd83fd6c13f53 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Wed, 4 Jul 2018 18:03:23 +0200 Subject: [PATCH 30/38] Cache le mot de passe de paiement comnpay --- cotisations/payment_methods/comnpay/aes_field.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cotisations/payment_methods/comnpay/aes_field.py b/cotisations/payment_methods/comnpay/aes_field.py index 1329b0a7..9a59b148 100644 --- a/cotisations/payment_methods/comnpay/aes_field.py +++ b/cotisations/payment_methods/comnpay/aes_field.py @@ -7,6 +7,7 @@ # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle # Copyright © 2018 Maël Kervella +# 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 @@ -22,10 +23,7 @@ # 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 """ @@ -36,6 +34,7 @@ from random import choice from Crypto.Cipher import AES from django.db import models +from django import forms from django.conf import settings EOD = '`%EofD%`' # This should be something that will not occur in strings @@ -66,6 +65,10 @@ def decrypt(key, s): return ss.split(bytes(EOD, 'utf-8'))[0] +class AESEncryptedFormField(forms.CharField): + widget = forms.PasswordInput(render_value=True) + + class AESEncryptedField(models.CharField): """ A Field that can be used in forms for adding the support of AES ecnrypted fields """ @@ -92,3 +95,8 @@ class AESEncryptedField(models.CharField): settings.AES_KEY, value )) + + def formfield(self, **kwargs): + defaults = {'form_class': AESEncryptedFormField} + defaults.update(kwargs) + return super().formfield(**defaults) From 20d9eead2b8b08114c58c042166f4a88a953cc96 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 5 Jul 2018 15:21:51 +0200 Subject: [PATCH 31/38] =?UTF-8?q?Plus=20de=20nom=20de=20paiement=20hardcod?= =?UTF-8?q?=C3=A9s=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/forms.py | 3 +- .../migrations/0038_paiement_is_balance.py | 29 +++++++++++++++++++ .../migrations/0039_auto_20180704_1147.py | 21 ++++++++++++++ cotisations/models.py | 18 ++++++++++++ cotisations/payment_methods/balance/models.py | 11 +++++++ cotisations/payment_methods/forms.py | 23 +++++++++++---- .../templates/cotisations/facture.html | 3 ++ cotisations/views.py | 4 +-- preferences/models.py | 5 +--- users/models.py | 2 +- 10 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 cotisations/migrations/0038_paiement_is_balance.py create mode 100644 cotisations/migrations/0039_auto_20180704_1147.py diff --git a/cotisations/forms.py b/cotisations/forms.py index b59eb6c3..5659d495 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -91,8 +91,7 @@ class CreditSoldeForm(NewFactureForm): super(CreditSoldeForm, self).__init__(*args, **kwargs) # TODO : change solde to balance self.fields['paiement'].queryset = Paiement.objects.exclude( - moyen='solde' - ).exclude(moyen='Solde') + is_balance=True) montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) diff --git a/cotisations/migrations/0038_paiement_is_balance.py b/cotisations/migrations/0038_paiement_is_balance.py new file mode 100644 index 00000000..bea02a17 --- /dev/null +++ b/cotisations/migrations/0038_paiement_is_balance.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-04 16:30 +from __future__ import unicode_literals + +from django.db import migrations, models + +def update_balance(apps, _): + Payment = apps.get_model('cotisations', 'Paiement') + try: + balance = Payment.objects.get(moyen="solde") + balance.is_balance = True + balance.save() + except Payment.DoesNotExist: + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0037_auto_20180703_1202'), + ] + + operations = [ + migrations.AddField( + model_name='paiement', + name='is_balance', + field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', verbose_name='Is user balance'), + ), + migrations.RunPython(update_balance) + ] diff --git a/cotisations/migrations/0039_auto_20180704_1147.py b/cotisations/migrations/0039_auto_20180704_1147.py new file mode 100644 index 00000000..3f982430 --- /dev/null +++ b/cotisations/migrations/0039_auto_20180704_1147.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-04 16:47 +from __future__ import unicode_literals + +import cotisations.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0038_paiement_is_balance'), + ] + + operations = [ + migrations.AlterField( + model_name='paiement', + name='is_balance', + field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', validators=[cotisations.models.check_no_balance], verbose_name='Is user balance'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 58e0f2d3..d6280045 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -594,6 +594,17 @@ class Banque(RevMixin, AclMixin, models.Model): return self.name +def check_no_balance(): + """This functions checks that no Paiement with is_balance=True exists + + :raises ValidationError: if such a Paiement exists. + """ + p = Paiement.objects.filter(is_balance=True) + if len(p)>0: + raise ValidationError( + _("There are already payment method(s) for user balance") + ) + # TODO : change Paiement to Payment class Paiement(RevMixin, AclMixin, models.Model): """ @@ -624,6 +635,13 @@ class Paiement(RevMixin, AclMixin, models.Model): default=False, verbose_name=_l("Is available for every user") ) + is_balance = models.BooleanField( + default=False, + editable=False, + verbose_name=_l("Is user balance"), + help_text=_l("There should be only one balance payment method."), + validators=[check_no_balance] + ) class Meta: permissions = ( diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py index 957244d7..ae4d2f0b 100644 --- a/cotisations/payment_methods/balance/models.py +++ b/cotisations/payment_methods/balance/models.py @@ -55,3 +55,14 @@ class BalancePayment(PaymentMethodMixin, models.Model): request, use_payment_method=False ) + + def valid_form(self, form): + p = Paiement.objects.filter(is_balance=True) + if len(p) > 0: + form.add_error( + 'payment_method', + _("There is already a payment type for user balance") + ) + + def alter_payment(self, payment): + self.payment.is_balance = True diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py index 37653c3a..2b712439 100644 --- a/cotisations/payment_methods/forms.py +++ b/cotisations/payment_methods/forms.py @@ -51,16 +51,27 @@ class PaymentMethodForm(forms.Form): else: self.fields = {} - def save(self, *args, payment=None, **kwargs): - commit = kwargs.pop('commit', True) + def clean(self): + super(PaymentMethodForm, self).clean() choice = self.cleaned_data['payment_method'] if choice=='': return choice = int(choice) model = PAYMENT_METHODS[choice].PaymentMethod form = forms.modelform_factory(model, fields='__all__')(self.data, prefix=self.prefix) - payment_method = form.save(commit=False) - payment_method.payment = payment + self.payment_method = form.save(commit=False) + if hasattr(self.payment_method, 'valid_form'): + self.payment_method.valid_form(self) + return self.cleaned_data + + + + def save(self, payment, *args, **kwargs): + commit = kwargs.pop('commit', True) + self.payment_method.payment = payment + if hasattr(self.payment_method, 'alter_payment'): + self.payment_method.alter_payment(payment) if commit: - payment_method.save() - return payment_method + payment.save() + self.payment_method.save() + return self.payment_method diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 03a4225b..081f3f51 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -31,6 +31,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %} {% bootstrap_form_errors factureform %} +{% if payment_method %} +{% bootstrap_form_errors payment_method %} +{% endif %} {% if title %}

{{title}}

{% endif %} diff --git a/cotisations/views.py b/cotisations/views.py index bb391d13..80f89ea1 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -409,7 +409,7 @@ def add_paiement(request): ) if payment.is_valid() and payment_method.is_valid(): payment = payment.save() - payment_method.save(payment=payment) + payment_method.save(payment) messages.success( request, _("The payment method has been successfully created.") @@ -688,7 +688,7 @@ def new_facture_solde(request, userid): """ user = request.user invoice = Facture(user=user) - payment, _created = Paiement.objects.get_or_create(moyen='Solde') + payment, _created = Paiement.objects.get_or_create(is_balance=True) invoice.paiement = payment # The template needs the list of articles (for the JS part) article_list = Article.objects.filter( diff --git a/preferences/models.py b/preferences/models.py index eeae30f9..4a22edbe 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -118,10 +118,7 @@ class OptionalUser(AclMixin, PreferencesModel): def clean(self): """Creation du mode de paiement par solde""" if self.user_solde: - p = cotisations.models.Paiement.objects.filter(moyen="Solde") - if not len(p): - c = cotisations.models.Paiement(moyen="Solde") - c.save() + cotisations.models.Paiement.objects.get_or_create(is_balance=True) @receiver(post_save, sender=OptionalUser) diff --git a/users/models.py b/users/models.py index 50f7c51f..cb26ace7 100644 --- a/users/models.py +++ b/users/models.py @@ -428,7 +428,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, def solde(self): """ Renvoie le solde d'un user. Somme les crédits de solde et retire les débit payés par solde""" - solde_objects = Paiement.objects.filter(moyen='solde') + solde_objects = Paiement.objects.filter(is_balance=True) somme_debit = Vente.objects.filter( facture__in=Facture.objects.filter( user=self, From 249cfeb7a63b371ac78823d7308f31eeb13262e6 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 5 Jul 2018 15:23:10 +0200 Subject: [PATCH 32/38] =?UTF-8?q?Valeur=20par=20d=C3=A9faut=20nulle=20pour?= =?UTF-8?q?=20le=20minimum=20de=20solde.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0040_auto_20180705_0822.py | 20 +++++++++++++++++++ cotisations/payment_methods/balance/models.py | 1 + 2 files changed, 21 insertions(+) create mode 100644 cotisations/migrations/0040_auto_20180705_0822.py diff --git a/cotisations/migrations/0040_auto_20180705_0822.py b/cotisations/migrations/0040_auto_20180705_0822.py new file mode 100644 index 00000000..31b0652a --- /dev/null +++ b/cotisations/migrations/0040_auto_20180705_0822.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-05 13:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0039_auto_20180704_1147'), + ] + + operations = [ + migrations.AlterField( + model_name='balancepayment', + name='minimum_balance', + field=models.DecimalField(decimal_places=2, default=0, help_text='The minimal amount of money allowed for the balance at the end of a payment. You can specify negative amount.', max_digits=5, verbose_name='Minimum balance'), + ), + ] diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py index ae4d2f0b..164136b9 100644 --- a/cotisations/payment_methods/balance/models.py +++ b/cotisations/payment_methods/balance/models.py @@ -27,6 +27,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): ), max_digits=5, decimal_places=2, + default=0, ) maximum_balance = models.DecimalField( verbose_name=_l("Maximum balance"), From 341778d72a92857b517eab8cd333b35caf2d441f Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 5 Jul 2018 15:48:07 +0200 Subject: [PATCH 33/38] Supprime tout ce qui ne sert plus pour les cotisations --- cotisations/forms.py | 70 +------- cotisations/models.py | 1 + .../cotisations/new_facture_solde.html | 158 ------------------ .../templates/cotisations/recharge.html | 45 ----- cotisations/urls.py | 6 - cotisations/views.py | 120 +------------ preferences/aes_field.py | 94 ----------- .../migrations/0036_auto_20180705_0840.py | 47 ++++++ .../migrations/0039_auto_20180115_0003.py | 1 - .../migrations/0040_auto_20180129_1745.py | 7 +- preferences/models.py | 44 ----- .../preferences/display_preferences.html | 62 +++---- 12 files changed, 77 insertions(+), 578 deletions(-) delete mode 100644 cotisations/templates/cotisations/new_facture_solde.html delete mode 100644 cotisations/templates/cotisations/recharge.html delete mode 100644 preferences/aes_field.py create mode 100644 preferences/migrations/0036_auto_20180705_0840.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 5659d495..02ee3e3c 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -5,6 +5,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# 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 @@ -78,24 +79,6 @@ class NewFactureForm(FormRevMixin, ModelForm): return cleaned_data -class CreditSoldeForm(NewFactureForm): - """ - Form used to make some operations on the user's balance if the option is - activated. - """ - class Meta(NewFactureForm.Meta): - model = Facture - fields = ['paiement', 'banque', 'cheque'] - - def __init__(self, *args, **kwargs): - super(CreditSoldeForm, self).__init__(*args, **kwargs) - # TODO : change solde to balance - self.fields['paiement'].queryset = Paiement.objects.exclude( - is_balance=True) - - montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) - - class SelectUserArticleForm(FormRevMixin, Form): """ Form used to select an article during the creation of an invoice for a @@ -300,57 +283,6 @@ class DelBanqueForm(FormRevMixin, Form): self.fields['banques'].queryset = Banque.objects.all() -# TODO : change facture to Invoice -class NewFactureSoldeForm(NewFactureForm): - """ - Form used to create an invoice - """ - - def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(NewFactureSoldeForm, self).__init__( - *args, - prefix=prefix, - **kwargs - ) - self.fields['cheque'].required = False - self.fields['banque'].required = False - self.fields['cheque'].label = _('Cheque number') - self.fields['banque'].empty_label = _("Not specified") - self.fields['paiement'].empty_label = \ - _("Select a payment method") - # TODO : change paiement to payment - paiement_list = Paiement.objects.filter(type_paiement=1) - if paiement_list: - self.fields['paiement'].widget\ - .attrs['data-cheque'] = paiement_list.first().id - - class Meta: - # TODO : change facture to invoice - model = Facture - # TODO : change paiement to payment and baque to bank - fields = ['paiement', 'banque'] - - def clean(self): - cleaned_data = super(NewFactureSoldeForm, self).clean() - # TODO : change paiement to payment - paiement = cleaned_data.get("paiement") - cheque = cleaned_data.get("cheque") - # TODO : change banque to bank - banque = cleaned_data.get("banque") - # TODO : change paiement to payment - if not paiement: - raise forms.ValidationError( - _("A payment method must be specified.") - ) - # TODO : change paiement and banque to payment and bank - elif paiement.type_paiement == "check" and not (cheque and banque): - raise forms.ValidationError( - _("A cheque number and a bank must be specified.") - ) - return cleaned_data - - # TODO : Better name and docstring class RechargeForm(FormRevMixin, Form): """ diff --git a/cotisations/models.py b/cotisations/models.py index d6280045..37d030ea 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -6,6 +6,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# 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 diff --git a/cotisations/templates/cotisations/new_facture_solde.html b/cotisations/templates/cotisations/new_facture_solde.html deleted file mode 100644 index dac68c54..00000000 --- a/cotisations/templates/cotisations/new_facture_solde.html +++ /dev/null @@ -1,158 +0,0 @@ -{% 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%} -{% load i18n %} - -{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} - -{% block content %} -{% bootstrap_form_errors venteform.management_form %} - - - {% csrf_token %} -

{% trans "New invoice" %}

- {{ venteform.management_form }} - -

{% trans "Invoice's articles" %}

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

- {% blocktrans %} - Total price : 0,00 € - {% endblocktrans %} -

- {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='star' %} - - - - -{% endblock %} - diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html deleted file mode 100644 index 6f4e9d9c..00000000 --- a/cotisations/templates/cotisations/recharge.html +++ /dev/null @@ -1,45 +0,0 @@ -{% 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%} -{% load i18n %} - -{% block title %}{% trans "Balance refill" %}{% endblock %} - -{% block content %} -

{% trans "Balance refill" %}

-

- {% blocktrans %} - Balance : {{ solde }} € - {% endblocktrans %} -

-
- {% csrf_token %} - {% bootstrap_form rechargeform %} - {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %} -
-{% endblock %} diff --git a/cotisations/urls.py b/cotisations/urls.py index ce6aa0f4..c906c54a 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -133,11 +133,5 @@ 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'), ] + payment_methods.urls.urlpatterns - diff --git a/cotisations/views.py b/cotisations/views.py index 80f89ea1..aa60aa8b 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -5,6 +5,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# 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 @@ -56,7 +57,7 @@ from re2o.acl import ( can_delete_set, can_change, ) -from preferences.models import OptionalUser, AssoOption, GeneralOption +from preferences.models import AssoOption, GeneralOption from .models import Facture, Article, Vente, Paiement, Banque from .forms import ( NewFactureForm, @@ -70,7 +71,6 @@ from .forms import ( NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - CreditSoldeForm, RechargeForm ) from .tex import render_invoice @@ -88,10 +88,6 @@ def new_facture(request, user, userid): A bit of JS is used in the template to add articles in a fancier way. If everything is correct, save each one of the articles, save the purchase object associated and finally the newly created invoice. - - TODO : The whole verification process should be moved to the model. This - function should only act as a dumb interface between the model and the - user. """ invoice = Facture(user=user) # The template needs the list of articles (for the JS part) @@ -670,118 +666,6 @@ def index(request): }) -# TODO : merge this function with new_facture() which is nearly the same -# TODO : change facture to invoice -@login_required -def new_facture_solde(request, userid): - """ - View called to create a new invoice when using the balance to pay. - Currently, send the list of available articles for the user along with - a formset of a new invoice (based on the `:forms:NewFactureForm()` form. - A bit of JS is used in the template to add articles in a fancier way. - If everything is correct, save each one of the articles, save the - purchase object associated and finally the newly created invoice. - - TODO : The whole verification process should be moved to the model. This - function should only act as a dumb interface between the model and the - user. - """ - user = request.user - invoice = Facture(user=user) - payment, _created = Paiement.objects.get_or_create(is_balance=True) - invoice.paiement = payment - # The template needs the list of articles (for the JS part) - 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 - # Check if at leat one article has been selected - if any(art.cleaned_data for art in articles): - user_balance = OptionalUser.get_cached_value('user_solde') - negative_balance = OptionalUser.get_cached_value('solde_negatif') - # If the paiement using balance has been activated, - # checking that the total price won't get the user under - # the authorized minimum (negative_balance) - if user_balance: - total_price = 0 - for art_item in articles: - if art_item.cleaned_data: - total_price += art_item.cleaned_data['article']\ - .prix*art_item.cleaned_data['quantity'] - if float(user.solde) - float(total_price) < negative_balance: - messages.error( - request, - _("The balance is too low for this operation.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - # Saving the invoice - invoice.save() - - # Building a purchase for each article sold - for art_item in articles: - if art_item.cleaned_data: - article = art_item.cleaned_data['article'] - quantity = art_item.cleaned_data['quantity'] - new_purchase = Vente.objects.create( - facture=invoice, - name=article.name, - prix=article.prix, - type_cotisation=article.type_cotisation, - duration=article.duration, - number=quantity - ) - new_purchase.save() - - # 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 successfully \ - 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 successuflly created.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - messages.error( - request, - _("You need to choose at least one article.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - - return form({ - 'venteform': article_formset, - 'articlelist': article_list - }, 'cotisations/new_facture_solde.html', request) - - # TODO : change solde to balance @login_required @can_create(Facture) diff --git a/preferences/aes_field.py b/preferences/aes_field.py deleted file mode 100644 index 1329b0a7..00000000 --- a/preferences/aes_field.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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 - )) diff --git a/preferences/migrations/0036_auto_20180705_0840.py b/preferences/migrations/0036_auto_20180705_0840.py new file mode 100644 index 00000000..9dc67dac --- /dev/null +++ b/preferences/migrations/0036_auto_20180705_0840.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-05 13:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0035_optionaluser_allow_self_subscription'), + ] + + operations = [ + migrations.RemoveField( + model_name='assooption', + name='payment', + ), + migrations.RemoveField( + model_name='assooption', + name='payment_id', + ), + migrations.RemoveField( + model_name='assooption', + name='payment_pass', + ), + migrations.RemoveField( + model_name='optionaluser', + name='allow_self_subscription', + ), + migrations.RemoveField( + model_name='optionaluser', + name='max_solde', + ), + migrations.RemoveField( + model_name='optionaluser', + name='min_online_payment', + ), + migrations.RemoveField( + model_name='optionaluser', + name='solde_negatif', + ), + migrations.RemoveField( + model_name='optionaluser', + name='user_solde', + ), + ] diff --git a/preferences/migrations/0039_auto_20180115_0003.py b/preferences/migrations/0039_auto_20180115_0003.py index 3dbe2b4c..f8da5c27 100644 --- a/preferences/migrations/0039_auto_20180115_0003.py +++ b/preferences/migrations/0039_auto_20180115_0003.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django.db import migrations, models -import preferences.aes_field class Migration(migrations.Migration): diff --git a/preferences/migrations/0040_auto_20180129_1745.py b/preferences/migrations/0040_auto_20180129_1745.py index dc7800f4..7b8248fd 100644 --- a/preferences/migrations/0040_auto_20180129_1745.py +++ b/preferences/migrations/0040_auto_20180129_1745.py @@ -3,7 +3,10 @@ from __future__ import unicode_literals from django.db import migrations, models -import preferences.aes_field +try: + import preferences.aes_field as aes_field +except ImportError: + import cotisations.payment_methods.comnpay.aes_field as aes_field class Migration(migrations.Migration): @@ -16,7 +19,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='assooption', name='payment_pass', - field=preferences.aes_field.AESEncryptedField(blank=True, max_length=255, null=True), + field=aes_field.AESEncryptedField(blank=True, max_length=255, null=True), ), migrations.AlterField( model_name='assooption', diff --git a/preferences/models.py b/preferences/models.py index 4a22edbe..2fd7d45a 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -35,8 +35,6 @@ import cotisations.models import machines.models from re2o.mixins import AclMixin -from .aes_field import AESEncryptedField - class PreferencesModel(models.Model): """ Base object for the Preferences objects @@ -67,22 +65,6 @@ class OptionalUser(AclMixin, PreferencesModel): PRETTY_NAME = "Options utilisateur" is_tel_mandatory = models.BooleanField(default=True) - user_solde = models.BooleanField(default=False) - solde_negatif = models.DecimalField( - max_digits=5, - decimal_places=2, - default=0 - ) - max_solde = models.DecimalField( - max_digits=5, - 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_club = models.BooleanField( default=False, @@ -96,13 +78,6 @@ class OptionalUser(AclMixin, PreferencesModel): default=False, help_text="Un nouvel utilisateur peut se créer son compte sur re2o" ) - allow_self_subscription = models.BooleanField( - default=False, - help_text=( - "Autoriser les utilisateurs à cotiser par eux mêmes via les" - " moyens de paiement permettant l'auto-cotisation." - ) - ) shell_default = models.OneToOneField( 'users.ListShell', on_delete=models.PROTECT, @@ -298,25 +273,6 @@ class AssoOption(AclMixin, PreferencesModel): blank=True, null=True ) - PAYMENT = ( - ('NONE', 'NONE'), - ('COMNPAY', 'COMNPAY'), - ) - payment = models.CharField( - max_length=255, - choices=PAYMENT, - default='NONE', - ) - payment_id = models.CharField( - max_length=255, - default='', - blank=True - ) - payment_pass = AESEncryptedField( - max_length=255, - null=True, - blank=True, - ) description = models.TextField( null=True, blank=True, diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 99e3e14f..09395b21 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -31,46 +31,30 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

Préférences utilisateur

- + Editer - +

- - - - - {% if useroptions.user_solde %} - - - {% endif %} - - + + - {% if useroptions.user_solde %} - - - - - - - {% endif %} - + - - - + + +
Téléphone obligatoirement requis {{ useroptions.is_tel_mandatory }}Activation du solde pour les utilisateurs{{ useroptions.user_solde }}
Champ gpg fingerprint {{ useroptions.gpg_fingerprint }}Solde négatif{{ useroptions.solde_negatif }}
Creations d'adhérents par tous {{ useroptions.all_can_create_adherent }}Creations de clubs par tous{{ useroptions.all_can_create_club }}Creations de clubs par tous{{ useroptions.all_can_create_club }}
Solde maximum{{ useroptions.max_solde }}Montant minimal de rechargement en ligne{{ useroptions.min_online_payment }}
Auto inscriptionAuto inscription {{ useroptions.self_adhesion }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
Shell par défaut des utilisateurs{{ useroptions.shell_default }}

Préférences machines

@@ -91,11 +75,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ machineoptions.max_lambdauser_aliases }} Support de l'ipv6 {{ machineoptions.ipv6_mode }} - - - Creation de machines + + + Creation de machines {{ machineoptions.create_machine }} - +

Préférences topologie

@@ -108,7 +92,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Politique générale de placement de vlan {{ topologieoptions.radius_general_policy }} - Ce réglage défini la politique vlan après acceptation radius : soit sur le vlan de la plage d'ip de la machine, soit sur un vlan prédéfini dans "Vlan où placer les machines après acceptation RADIUS" + Ce réglage défini la politique vlan après acceptation radius : soit sur le vlan de la plage d'ip de la machine, soit sur un vlan prédéfini dans "Vlan où placer les machines après acceptation RADIUS" @@ -144,12 +128,12 @@ with this program; if not, write to the Free Software Foundation, Inc., Temps avant expiration du lien de reinitialisation de mot de passe (en heures) {{ generaloptions.req_expire_hrs }} - + Message global affiché sur le site {{ generaloptions.general_message }} Résumé des CGU {{ generaloptions.GTU_sum_up }} - + CGU {{generaloptions.GTU}} @@ -171,8 +155,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Adresse - {{ assooptions.adresse1 }}
- {{ assooptions.adresse2 }} + {{ assooptions.adresse1 }}
+ {{ assooptions.adresse2 }} Contact mail {{ assooptions.contact }} @@ -185,13 +169,9 @@ 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 }} - - - Description de l'association - {{ assooptions.description | safe }} - + Description de l'association + {{ assooptions.description | safe }} +

Messages personalisé dans les mails

@@ -205,7 +185,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Mail de bienvenue (Français) {{ mailmessageoptions.welcome_mail_fr | safe }} - + Mail de bienvenue (Anglais) {{ mailmessageoptions.welcome_mail_en | safe }} From e3f3839a3707f5ebc04f27238dc1d28b6386c971 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 7 Jul 2018 20:53:42 +0200 Subject: [PATCH 34/38] Suppression d'un champ inutile --- cotisations/payment_methods/mixins.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cotisations/payment_methods/mixins.py b/cotisations/payment_methods/mixins.py index 62ebbf32..cbbe1427 100644 --- a/cotisations/payment_methods/mixins.py +++ b/cotisations/payment_methods/mixins.py @@ -1,15 +1,5 @@ -from django.db import models - -from cotisations.models import Paiement - - class PaymentMethodMixin: """The base class for payment models. They should inherit from this.""" - payment = models.OneToOneField( - Paiement, - related_name='payment_method', - editable=False - ) def end_payment(self, invoice, request): """Redefine this method in order to get a different ending to the From 64d6fc5a989ab32f5717dc16981e0288779b0c25 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 7 Jul 2018 23:09:03 +0200 Subject: [PATCH 35/38] Supprime l'ancienne valeur du comnpay pass --- cotisations/migrations/0032_chequepayment_comnpaypayment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cotisations/migrations/0032_chequepayment_comnpaypayment.py b/cotisations/migrations/0032_chequepayment_comnpaypayment.py index e0717a05..729b6f2e 100644 --- a/cotisations/migrations/0032_chequepayment_comnpaypayment.py +++ b/cotisations/migrations/0032_chequepayment_comnpaypayment.py @@ -41,6 +41,7 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunSQL('update preferences_assooption set payment_pass="" where id=1;'), migrations.CreateModel( name='ChequePayment', fields=[ From 2951b648a5dfe604555529b5e4ddad6045b3a161 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 8 Jul 2018 00:01:06 +0200 Subject: [PATCH 36/38] =?UTF-8?q?Documentation=20des=20paiements=20personn?= =?UTF-8?q?alis=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/payment_methods/__init__.py | 129 ++++++++++++++++++ .../payment_methods/balance/__init__.py | 20 +++ cotisations/payment_methods/balance/models.py | 26 ++++ .../payment_methods/cheque/__init__.py | 20 +++ cotisations/payment_methods/cheque/forms.py | 21 ++- cotisations/payment_methods/cheque/models.py | 25 ++++ cotisations/payment_methods/cheque/urls.py | 20 +++ cotisations/payment_methods/cheque/views.py | 21 +++ .../payment_methods/comnpay/__init__.py | 20 +++ .../payment_methods/comnpay/aes_field.py | 14 +- cotisations/payment_methods/comnpay/models.py | 21 +++ cotisations/payment_methods/comnpay/views.py | 30 +++- cotisations/payment_methods/forms.py | 72 +++++++--- cotisations/payment_methods/mixins.py | 24 +++- cotisations/payment_methods/urls.py | 20 +++ cotisations/utils.py | 23 ++++ 16 files changed, 479 insertions(+), 27 deletions(-) diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py index 422bd069..b78a86fd 100644 --- a/cotisations/payment_methods/__init__.py +++ b/cotisations/payment_methods/__init__.py @@ -1,3 +1,132 @@ +# -*- mode: python; 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 © 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. +""" +# Custom Payment methods + +When creating an invoice with a classic payment method, the creation view calls +the `end_payment` method of the `Payment` object of the invoice. This method +checks for a payment method associated to the `Payment` and if nothing is +found, adds a message for payment confirmation and redirects the user towards +their profil page. This is fine for most of the payment method, but you might +want to define custom payment methods. As an example for negociating with an +other server for online payment or updating some fields in your models. + +# Defining a custom payment method +To define a custom payment method, you can add a Python module to +`cotisations/payment_methods/`. This module should be organized like +a Django application. +As an example, if you want to add the payment method `foo`. + +## Basic + +The first thing to do is to create a `foo` Python module with a `models.py`. + +``` +payment_methods +├── foo +│ ├── __init__.py +│ └── models.py +├── forms.py +├── __init__.py +├── mixins.py +└── urls.py +``` + +Then, in `models.py` you could add a model like this : +```python +from django.db import models + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + + +# The `PaymentMethodMixin` defines the default `end_payment` +class FooPayment(PaymentMethodMixin, models.Model): + + # This field is required, it is used by `Paiement` in order to + # determine if a payment method is associated to it. + payment = models.OneToOneField( + Paiement, + on_delete=models.CASCADE, + related_name='payment_method', + editable=False + ) +``` + +And in `__init__.py` : +```python +from . import models +NAME = "FOO" # Name displayed when you crate a payment type +PaymentMethod = models.FooPayment # You must define this alias +``` + +Then you just have to register your payment method in +`payment_methods/__init__.py` in the `PAYMENT_METHODS` list : + +``` +from . import ... # Some existing imports +from . import foo + +PAYMENT_METHODS = [ + # Some already registered payment methods... + foo +] +``` + +And... that's it, you can use your new payment method after running +`makemigrations` and `migrate`. + +But this payment method is not really usefull, since it does noting ! + +## A payment method which does something + +You have to redefine the `end_payment` method. Here is its prototype : + +```python +def end_payment(self, invoice, request): + pass +``` + +With `invoice` the invoice being created and `request` the request which +created it. This method has to return an HttpResponse-like object. + +## Additional views + +You can add specific urls for your payment method like in any django app. To +register these urls, modify `payment_methods/urls.py`. + +## Alter the `Paiement` object after creation + +You can do that by adding a `alter_payment(self, payment)` +method to your model. + +## Validate the creation field + +You may want to perform some additionals verifications on the form +creating the payment. You can do that by adding a `valid_form(self, form)` +method to your model, where `form` is an instance of +`cotisations.payment_methods.forms.PaymentMethodForm`. +""" + + from . import comnpay, cheque, balance, urls PAYMENT_METHODS = [ diff --git a/cotisations/payment_methods/balance/__init__.py b/cotisations/payment_methods/balance/__init__.py index adfca187..cacd73f7 100644 --- a/cotisations/payment_methods/balance/__init__.py +++ b/cotisations/payment_methods/balance/__init__.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. """ This module contains a method to pay online using user balance. """ diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py index 164136b9..4d2875d8 100644 --- a/cotisations/payment_methods/balance/models.py +++ b/cotisations/payment_methods/balance/models.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. from django.db import models from django.shortcuts import redirect from django.urls import reverse @@ -16,6 +36,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): """ payment = models.OneToOneField( Paiement, + on_delete=models.CASCADE, related_name='payment_method', editable=False ) @@ -38,6 +59,9 @@ class BalancePayment(PaymentMethodMixin, models.Model): ) def end_payment(self, invoice, request): + """Changes the user's balance to pay the invoice. If it is not + possible, shows an error and invalidates the invoice. + """ user = invoice.user total_price = invoice.prix_total() if float(user.solde) - float(total_price) < self.minimum_balance: @@ -58,6 +82,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): ) def valid_form(self, form): + """Checks that there is not already a balance payment method.""" p = Paiement.objects.filter(is_balance=True) if len(p) > 0: form.add_error( @@ -66,4 +91,5 @@ class BalancePayment(PaymentMethodMixin, models.Model): ) def alter_payment(self, payment): + """Register the payment as a balance payment.""" self.payment.is_balance = True diff --git a/cotisations/payment_methods/cheque/__init__.py b/cotisations/payment_methods/cheque/__init__.py index a6fef640..9eb17b09 100644 --- a/cotisations/payment_methods/cheque/__init__.py +++ b/cotisations/payment_methods/cheque/__init__.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. """ This module contains a method to pay online using cheque. """ diff --git a/cotisations/payment_methods/cheque/forms.py b/cotisations/payment_methods/cheque/forms.py index ae816a4b..37942687 100644 --- a/cotisations/payment_methods/cheque/forms.py +++ b/cotisations/payment_methods/cheque/forms.py @@ -1,5 +1,24 @@ +# -*- mode: python; 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 © 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. from django import forms -from django.utils.translation import ugettext_lazy as _l from re2o.mixins import FormRevMixin from cotisations.models import Facture as Invoice diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py index 29c86a41..bdb4e594 100644 --- a/cotisations/payment_methods/cheque/models.py +++ b/cotisations/payment_methods/cheque/models.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. from django.db import models from django.shortcuts import redirect from django.urls import reverse @@ -12,10 +32,15 @@ class ChequePayment(PaymentMethodMixin, models.Model): """ payment = models.OneToOneField( Paiement, + on_delete=models.CASCADE, related_name='payment_method', editable=False ) + def end_payment(self, invoice, request): + """Invalidates the invoice then redirect the user towards a view asking + for informations to add to the invoice before validating it. + """ invoice.valid = False invoice.save() return redirect(reverse( diff --git a/cotisations/payment_methods/cheque/urls.py b/cotisations/payment_methods/cheque/urls.py index f7cc39d5..54fe6a50 100644 --- a/cotisations/payment_methods/cheque/urls.py +++ b/cotisations/payment_methods/cheque/urls.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. from django.conf.urls import url from . import views diff --git a/cotisations/payment_methods/cheque/views.py b/cotisations/payment_methods/cheque/views.py index bcf707f9..47654990 100644 --- a/cotisations/payment_methods/cheque/views.py +++ b/cotisations/payment_methods/cheque/views.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. """Payment Here are defined some views dedicated to cheque payement. @@ -17,6 +37,7 @@ from .forms import InvoiceForm @login_required def cheque(request, invoice_pk): + """This view validate an invoice with the data from a cheque.""" 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): diff --git a/cotisations/payment_methods/comnpay/__init__.py b/cotisations/payment_methods/comnpay/__init__.py index 58433c80..7c80311d 100644 --- a/cotisations/payment_methods/comnpay/__init__.py +++ b/cotisations/payment_methods/comnpay/__init__.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. """ This module contains a method to pay online using comnpay. """ diff --git a/cotisations/payment_methods/comnpay/aes_field.py b/cotisations/payment_methods/comnpay/aes_field.py index 9a59b148..5708eeef 100644 --- a/cotisations/payment_methods/comnpay/aes_field.py +++ b/cotisations/payment_methods/comnpay/aes_field.py @@ -79,14 +79,22 @@ class AESEncryptedField(models.CharField): def to_python(self, value): if value is None: return None - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') + try: + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + except Exception as e: + v = decrypt(settings.AES_KEY, binascii.a2b_base64(value)) + raise ValueError(v) def from_db_value(self, value, *args, **kwargs): if value is None: return value - return decrypt(settings.AES_KEY, + try: + return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8') + except Exception as e: + v = decrypt(settings.AES_KEY, binascii.a2b_base64(value)) + raise ValueError(v) def get_prep_value(self, value): if value is None: diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index fefa3587..cbc93db1 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. from django.db import models from django.shortcuts import render from django.urls import reverse @@ -17,6 +37,7 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): """ payment = models.OneToOneField( Paiement, + on_delete=models.CASCADE, related_name='payment_method', editable=False ) diff --git a/cotisations/payment_methods/comnpay/views.py b/cotisations/payment_methods/comnpay/views.py index 814a51c8..89966b48 100644 --- a/cotisations/payment_methods/comnpay/views.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -1,6 +1,26 @@ +# -*- mode: python; 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 © 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. """Payment -Here are defined some views dedicated to online payement. +Here are the views needed by comnpay """ from collections import OrderedDict @@ -23,7 +43,8 @@ from .models import ComnpayPayment @login_required def accept_payment(request, factureid): """ - The view called when an online payment has been accepted. + The view where the user is redirected when a comnpay payment has been + accepted. """ invoice = get_object_or_404(Facture, id=factureid) if invoice.valid: @@ -55,7 +76,8 @@ def accept_payment(request, factureid): @login_required def refuse_payment(request): """ - The view called when an online payment has been refused. + The view where the user is redirected when a comnpay payment has been + refused. """ messages.error( request, @@ -72,7 +94,7 @@ def ipn(request): """ The view called by Comnpay server to validate the transaction. 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 + Comnpay with 400 response if not or with a 200 response if yes. """ p = Transaction() order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', ) diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py index 2b712439..be8df2d1 100644 --- a/cotisations/payment_methods/forms.py +++ b/cotisations/payment_methods/forms.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. from django import forms from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _l @@ -6,6 +26,19 @@ from . import PAYMENT_METHODS from cotisations.utils import find_payment_method def payment_method_factory(payment, *args, creation=True, **kwargs): + """This function finds the right payment method form for a given payment. + + If the payment has a payment method, returns a ModelForm of it. Else if + it is the creation of the payment, a `PaymentMethodForm`. + Else an empty form. + + :param payment: The payment + :param *args: arguments passed to the form + :param creation: Should be True if you are creating the payment + :param **kwargs: passed to the form + + :returns: A form + """ payment_method = kwargs.pop('instance', find_payment_method(payment)) if payment_method is not None: return forms.modelform_factory(type(payment_method), fields='__all__')( @@ -14,16 +47,14 @@ def payment_method_factory(payment, *args, creation=True, **kwargs): **kwargs ) elif creation: - return PaymentMethodForm(payment_method, *args, **kwargs) + return PaymentMethodForm(*args, **kwargs) else: return forms.Form() class PaymentMethodForm(forms.Form): """A special form which allows you to add a payment method to a `Payment` - objects if it hasn't one yet, or to edit the existing payment method. - - To do so it replaces itself with a `modelform_factory`. + object. """ payment_method = forms.ChoiceField( @@ -35,23 +66,24 @@ class PaymentMethodForm(forms.Form): required=False ) - def __init__(self, payment_method, *args, **kwargs): + def __init__(self, *args, **kwargs): super(PaymentMethodForm, self).__init__(*args, **kwargs) - if payment_method is None: - prefix = kwargs.get('prefix', None) - self.fields['payment_method'].choices = [(i,p.NAME) for (i,p) in enumerate(PAYMENT_METHODS)] - self.fields['payment_method'].choices.insert(0, ('', _l('no'))) - self.fields['payment_method'].widget.attrs = { - 'id': 'paymentMethodSelect' - } - self.templates = [ - forms.modelform_factory(p.PaymentMethod, fields='__all__')(prefix=prefix) - for p in PAYMENT_METHODS - ] - else: - self.fields = {} + prefix = kwargs.get('prefix', None) + self.fields['payment_method'].choices = [(i,p.NAME) for (i,p) in enumerate(PAYMENT_METHODS)] + self.fields['payment_method'].choices.insert(0, ('', _l('no'))) + self.fields['payment_method'].widget.attrs = { + 'id': 'paymentMethodSelect' + } + self.templates = [ + forms.modelform_factory(p.PaymentMethod, fields='__all__')(prefix=prefix) + for p in PAYMENT_METHODS + ] def clean(self): + """A classic `clean` method, except that it replaces + `self.payment_method` by the payment method object if one has been + found. Tries to call `payment_method.valid_form` if it exists. + """ super(PaymentMethodForm, self).clean() choice = self.cleaned_data['payment_method'] if choice=='': @@ -67,6 +99,10 @@ class PaymentMethodForm(forms.Form): def save(self, payment, *args, **kwargs): + """Saves the payment method. + + Tries to call `payment_method.alter_payment` if it exists. + """ commit = kwargs.pop('commit', True) self.payment_method.payment = payment if hasattr(self.payment_method, 'alter_payment'): diff --git a/cotisations/payment_methods/mixins.py b/cotisations/payment_methods/mixins.py index cbbe1427..12503e05 100644 --- a/cotisations/payment_methods/mixins.py +++ b/cotisations/payment_methods/mixins.py @@ -1,5 +1,27 @@ +# -*- mode: python; 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 © 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. + + class PaymentMethodMixin: - """The base class for payment models. They should inherit from this.""" + """A simple mixin to avoid redefining end_payment if you don't need to""" def end_payment(self, invoice, request): """Redefine this method in order to get a different ending to the diff --git a/cotisations/payment_methods/urls.py b/cotisations/payment_methods/urls.py index 9d35846f..20e50255 100644 --- a/cotisations/payment_methods/urls.py +++ b/cotisations/payment_methods/urls.py @@ -1,3 +1,23 @@ +# -*- mode: python; 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 © 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. from django.conf.urls import include, url from . import comnpay, cheque diff --git a/cotisations/utils.py b/cotisations/utils.py index 01881af3..f36b376f 100644 --- a/cotisations/utils.py +++ b/cotisations/utils.py @@ -1,4 +1,27 @@ +# -*- mode: python; 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 © 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. + + def find_payment_method(payment): + """Finds the payment method associated to the payment if it exists.""" from cotisations.payment_methods import PAYMENT_METHODS for method in PAYMENT_METHODS: try: From 9f2a895104e0d46188c159f482eafd89d0e17dc2 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 8 Jul 2018 00:12:26 +0200 Subject: [PATCH 37/38] Affiche les articles disponibles pour tout le monde --- cotisations/templates/cotisations/aff_article.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index eb4ec0a7..ad044f55 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Cotisation type" %} {% trans "Duration (month)" %} {% trans "Concerned users" %} + {% trans "Available for everyone" %} @@ -42,7 +43,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ article.prix }} {{ article.type_cotisation }} {{ article.duration }} - {{ article.type_user }} + {{ article.type_user }} + {{ article.available_for_everyone }} {% can_edit article %}
From 739b6884e03f6fff268d57fe38321b197bcaf3d2 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 9 Jul 2018 20:35:25 +0200 Subject: [PATCH 38/38] Rend le aes_field disponible pour tout le monde et mon chibracounet facilement --- cotisations/migrations/0032_chequepayment_comnpaypayment.py | 4 ++-- cotisations/migrations/0034_auto_20180703_0929.py | 4 ++-- cotisations/payment_methods/comnpay/models.py | 2 +- preferences/migrations/0040_auto_20180129_1745.py | 2 +- {cotisations/payment_methods/comnpay => re2o}/aes_field.py | 0 5 files changed, 6 insertions(+), 6 deletions(-) rename {cotisations/payment_methods/comnpay => re2o}/aes_field.py (100%) diff --git a/cotisations/migrations/0032_chequepayment_comnpaypayment.py b/cotisations/migrations/0032_chequepayment_comnpaypayment.py index 729b6f2e..9c3fab02 100644 --- a/cotisations/migrations/0032_chequepayment_comnpaypayment.py +++ b/cotisations/migrations/0032_chequepayment_comnpaypayment.py @@ -2,7 +2,7 @@ # Generated by Django 1.10.7 on 2018-07-02 18:56 from __future__ import unicode_literals -import cotisations.payment_methods.comnpay.aes_field +import re2o.aes_field import cotisations.payment_methods.mixins from django.db import migrations, models import django.db.models.deletion @@ -55,7 +55,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('payment_credential', 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_pass', re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True)), ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), ], bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), diff --git a/cotisations/migrations/0034_auto_20180703_0929.py b/cotisations/migrations/0034_auto_20180703_0929.py index 1e726cd1..aa3ca10f 100644 --- a/cotisations/migrations/0034_auto_20180703_0929.py +++ b/cotisations/migrations/0034_auto_20180703_0929.py @@ -2,7 +2,7 @@ # Generated by Django 1.10.7 on 2018-07-03 14:29 from __future__ import unicode_literals -import cotisations.payment_methods.comnpay.aes_field +import re2o.aes_field from django.db import migrations, models @@ -21,6 +21,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='comnpaypayment', name='payment_pass', - field=cotisations.payment_methods.comnpay.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay Secret Key'), + field=re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay Secret Key'), ), ] diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index cbc93db1..d32e0a06 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -27,7 +27,7 @@ from django.utils.translation import ugettext_lazy as _l from cotisations.models import Paiement from cotisations.payment_methods.mixins import PaymentMethodMixin -from .aes_field import AESEncryptedField +from re2o.aes_field import AESEncryptedField from .comnpay import Transaction diff --git a/preferences/migrations/0040_auto_20180129_1745.py b/preferences/migrations/0040_auto_20180129_1745.py index 7b8248fd..7e657079 100644 --- a/preferences/migrations/0040_auto_20180129_1745.py +++ b/preferences/migrations/0040_auto_20180129_1745.py @@ -6,7 +6,7 @@ from django.db import migrations, models try: import preferences.aes_field as aes_field except ImportError: - import cotisations.payment_methods.comnpay.aes_field as aes_field + import re2o.aes_field as aes_field class Migration(migrations.Migration): diff --git a/cotisations/payment_methods/comnpay/aes_field.py b/re2o/aes_field.py similarity index 100% rename from cotisations/payment_methods/comnpay/aes_field.py rename to re2o/aes_field.py