mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-25 22:22:26 +00:00
Merge branch 'fix_online_payment' into 'master'
Refactorisation des moyens de paiement See merge request federez/re2o!174
This commit is contained in:
commit
cfa6fe097d
45 changed files with 1757 additions and 1113 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -95,7 +95,6 @@ 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 145: Fix #117 : Use unix_name instead of name for ldap groups
|
||||
|
||||
Fix a mixing between unix_name and name for groups
|
||||
|
@ -110,3 +109,14 @@ After this modification you need to:
|
|||
```bash
|
||||
sudo nslcd -i groups
|
||||
```
|
||||
|
||||
## 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, please go to the wiki.
|
||||
|
|
|
@ -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
|
||||
|
@ -41,75 +42,49 @@ from django.forms import ModelForm, Form
|
|||
from django.core.validators import MinValueValidator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _l
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
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):
|
||||
class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
|
||||
"""
|
||||
Form used to create a new invoice by using a payment method, a bank and a
|
||||
cheque number.
|
||||
Form used to manage and create an invoice and its fields.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
def __init__(self, *args, creation=False, **kwargs):
|
||||
user = kwargs['user']
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
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
|
||||
self.fields['cheque'].required = False
|
||||
self.fields['banque'].required = False
|
||||
self.fields['cheque'].label = _("Cheque number")
|
||||
self.fields['banque'].empty_label = _("Not specified")
|
||||
super(FactureForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
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
|
||||
self.fields['paiement'].queryset = Paiement.find_allowed_payments(user)
|
||||
if not creation:
|
||||
self.fields['user'].label = _("Member")
|
||||
self.fields['user'].empty_label = \
|
||||
_("Select the proprietary member")
|
||||
self.fields['valid'].label = _("Validated invoice")
|
||||
else:
|
||||
self.fields = {'paiement': self.fields['paiement']}
|
||||
|
||||
class Meta:
|
||||
model = Facture
|
||||
fields = ['paiement', 'banque', 'cheque']
|
||||
fields = '__all__'
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(NewFactureForm, self).clean()
|
||||
cleaned_data = super(FactureForm, 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
|
||||
|
||||
|
||||
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(
|
||||
moyen='solde'
|
||||
).exclude(moyen='Solde')
|
||||
|
||||
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.
|
||||
|
@ -127,6 +102,11 @@ class SelectUserArticleForm(
|
|||
required=True
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
super(SelectUserArticleForm, self).__init__(*args, **kwargs)
|
||||
self.fields['article'].queryset = Article.find_allowed_articles(user)
|
||||
|
||||
|
||||
class SelectClubArticleForm(Form):
|
||||
"""
|
||||
|
@ -146,6 +126,10 @@ class SelectClubArticleForm(Form):
|
|||
required=True
|
||||
)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(SelectClubArticleForm, self).__init__(*args, **kwargs)
|
||||
self.fields['article'].queryset = Article.find_allowed_articles(user)
|
||||
|
||||
|
||||
# TODO : change Facture to Invoice
|
||||
class NewFactureFormPdf(Form):
|
||||
|
@ -167,26 +151,6 @@ class NewFactureFormPdf(Form):
|
|||
)
|
||||
|
||||
|
||||
# TODO : change Facture to Invoice
|
||||
class EditFactureForm(FieldPermissionFormMixin, NewFactureForm):
|
||||
"""
|
||||
Form used to edit an invoice and its fields : payment method, bank,
|
||||
user associated, ...
|
||||
"""
|
||||
class Meta(NewFactureForm.Meta):
|
||||
# TODO : change Facture to Invoice
|
||||
model = Facture
|
||||
fields = '__all__'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# TODO : change Facture to Invoice
|
||||
super(EditFactureForm, self).__init__(*args, **kwargs)
|
||||
self.fields['user'].label = _("Member")
|
||||
self.fields['user'].empty_label = \
|
||||
_("Select the proprietary member")
|
||||
self.fields['valid'].label = _("Validated invoice")
|
||||
|
||||
|
||||
class ArticleForm(FormRevMixin, ModelForm):
|
||||
"""
|
||||
Form used to create an article.
|
||||
|
@ -231,17 +195,12 @@ 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', 'available_for_everyone']
|
||||
|
||||
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
|
||||
|
@ -304,56 +263,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):
|
||||
"""
|
||||
|
@ -364,34 +273,31 @@ 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')
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
self.user = user
|
||||
super(RechargeForm, self).__init__(*args, **kwargs)
|
||||
self.fields['payment'].empty_label = \
|
||||
_("Select a payment method")
|
||||
self.fields['payment'].queryset = Paiement.find_allowed_payments(user)
|
||||
|
||||
def clean_value(self):
|
||||
def clean(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'):
|
||||
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'
|
||||
)
|
||||
}
|
||||
)
|
||||
if value + self.user.solde > \
|
||||
OptionalUser.get_cached_value('max_solde'):
|
||||
balance_method = get_object_or_404(balance.PaymentMethod)
|
||||
if balance_method.maximum_balance is not None and \
|
||||
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
|
||||
return self.cleaned_data
|
||||
|
|
132
cotisations/migrations/0030_custom_payment.py
Normal file
132
cotisations/migrations/0030_custom_payment.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2018-07-02 18:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re2o.aes_field
|
||||
import cotisations.payment_methods.mixins
|
||||
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()
|
||||
try:
|
||||
payment = Payment.objects.get(
|
||||
moyen='Rechargement en ligne'
|
||||
)
|
||||
except Payment.DoesNotExist:
|
||||
return
|
||||
comnpay = ComnpayPayment()
|
||||
comnpay.payment_user = options.payment_id
|
||||
comnpay.payment = payment
|
||||
comnpay.save()
|
||||
payment.moyen = "ComnPay"
|
||||
|
||||
payment.save()
|
||||
|
||||
|
||||
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')
|
||||
|
||||
try:
|
||||
solde = Payment.objects.get(moyen="solde")
|
||||
except Payment.DoesNotExist:
|
||||
return
|
||||
balance = BalancePayment()
|
||||
balance.payment = solde
|
||||
balance.minimum_balance = options.solde_negatif
|
||||
balance.maximum_balance = options.max_solde
|
||||
solde.is_balance = True
|
||||
balance.save()
|
||||
solde.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('preferences', '0044_remove_payment_pass'),
|
||||
('cotisations', '0029_auto_20180414_2056'),
|
||||
]
|
||||
|
||||
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.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.AddField(
|
||||
model_name='paiement',
|
||||
name='available_for_everyone',
|
||||
field=models.BooleanField(default=False, verbose_name='Is available for every user'),
|
||||
),
|
||||
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', validators=[cotisations.models.check_no_balance]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='available_for_everyone',
|
||||
field=models.BooleanField(default=False, verbose_name='Is available for every user'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ChequePayment',
|
||||
fields=[
|
||||
('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.mixins.PaymentMethodMixin, models.Model),
|
||||
options={'verbose_name': 'Cheque'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ComnpayPayment',
|
||||
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, verbose_name='ComNpay VAD Number')),
|
||||
('payment_pass', re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay Secret Key')),
|
||||
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
|
||||
('minimum_payment', models.DecimalField(decimal_places=2, default=1, help_text='The minimal amount of money you have to use when paying with ComNpay', max_digits=5, verbose_name='Minimum payment')),
|
||||
],
|
||||
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
|
||||
options={'verbose_name': 'ComNpay'},
|
||||
),
|
||||
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, 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')),
|
||||
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
|
||||
('maximum_balance', 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', null=True, blank=True)),
|
||||
('credit_balance_allowed', models.BooleanField(default=False, verbose_name='Allow user to credit their balance')),
|
||||
],
|
||||
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
|
||||
options={'verbose_name': 'User Balance'},
|
||||
),
|
||||
migrations.RunPython(add_comnpay),
|
||||
migrations.RunPython(add_cheque),
|
||||
migrations.RunPython(add_solde),
|
||||
migrations.RemoveField(
|
||||
model_name='paiement',
|
||||
name='type_paiement',
|
||||
),
|
||||
|
||||
]
|
|
@ -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
|
||||
|
@ -42,11 +43,17 @@ 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
|
||||
from re2o.mixins import AclMixin, RevMixin
|
||||
|
||||
from cotisations.utils import find_payment_method
|
||||
from cotisations.validators import check_no_balance
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||
|
@ -131,7 +138,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
|
||||
|
@ -143,12 +150,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'] or 0
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
|
@ -157,7 +164,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):
|
||||
|
@ -213,6 +220,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 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
|
||||
an invoice or if the `options.allow_self_subscription` is set.
|
||||
"""
|
||||
if user_request.has_perm('cotisations.add_facture'):
|
||||
return True, None
|
||||
if len(Paiement.find_allowed_payments(user_request)) <= 0:
|
||||
return False, _("There are no payment types which you can use.")
|
||||
if len(Article.find_allowed_articles(user_request)) <= 0:
|
||||
return False, _("There are no article that you can buy.")
|
||||
return True, None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Facture, self).__init__(*args, **kwargs)
|
||||
self.field_permissions = {
|
||||
|
@ -341,12 +364,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:
|
||||
|
@ -357,7 +380,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):
|
||||
|
@ -380,7 +403,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
|
||||
|
@ -501,12 +524,17 @@ class Article(RevMixin, AclMixin, models.Model):
|
|||
max_length=255,
|
||||
verbose_name=_l("Type of cotisation")
|
||||
)
|
||||
available_for_everyone = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_l("Is available for every user")
|
||||
)
|
||||
|
||||
unique_together = ('name', 'type_user')
|
||||
|
||||
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"
|
||||
|
@ -524,6 +552,35 @@ 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.
|
||||
|
||||
Args:
|
||||
self: The article
|
||||
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')
|
||||
or user.has_perm('cotisations.add_facture'),
|
||||
_("You cannot buy this Article.")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def find_allowed_articles(cls, user):
|
||||
"""Finds every allowed articles for an user.
|
||||
|
||||
Args:
|
||||
user: The user requesting articles.
|
||||
"""
|
||||
if user.has_perm('cotisations.buy_every_article'):
|
||||
return cls.objects.all()
|
||||
return cls.objects.filter(available_for_everyone=True)
|
||||
|
||||
|
||||
class Banque(RevMixin, AclMixin, models.Model):
|
||||
"""
|
||||
|
@ -557,29 +614,29 @@ class Paiement(RevMixin, AclMixin, models.Model):
|
|||
invoice. It's easier to know this information when doing the accouts.
|
||||
It is represented by:
|
||||
* a name
|
||||
* a type (used for the type 'cheque' which implies the use of a bank
|
||||
and an account number in related models)
|
||||
"""
|
||||
|
||||
PAYMENT_TYPES = (
|
||||
(0, _l("Standard")),
|
||||
(1, _l("Cheque")),
|
||||
)
|
||||
|
||||
# TODO : change moyen to method
|
||||
moyen = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_l("Method")
|
||||
)
|
||||
type_paiement = models.IntegerField(
|
||||
choices=PAYMENT_TYPES,
|
||||
default=0,
|
||||
verbose_name=_l("Payment type")
|
||||
available_for_everyone = models.BooleanField(
|
||||
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 = (
|
||||
('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")
|
||||
|
@ -593,16 +650,79 @@ class Paiement(RevMixin, AclMixin, models.Model):
|
|||
"""
|
||||
self.moyen = self.moyen.title()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def end_payment(self, invoice, request, use_payment_method=True):
|
||||
"""
|
||||
Override of the herited save function to be sure only one payment
|
||||
method of type 'cheque' exists.
|
||||
The general way of ending a payment.
|
||||
|
||||
Args:
|
||||
invoice: The invoice being created.
|
||||
request: Request sent by the user.
|
||||
use_payment_method: If this flag is set to True and`self` has
|
||||
an attribute `payment_method`, returns the result of
|
||||
`self.payment_method.end_payment(invoice, request)`
|
||||
|
||||
Returns:
|
||||
An `HttpResponse`-like object.
|
||||
"""
|
||||
if Paiement.objects.filter(type_paiement=1).count() > 1:
|
||||
raise ValidationError(
|
||||
_("You cannot have multiple payment method of type cheque")
|
||||
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
|
||||
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': invoice.user.pseudo,
|
||||
'end_date': invoice.user.end_adhesion()
|
||||
}
|
||||
)
|
||||
super(Paiement, self).save(*args, **kwargs)
|
||||
# Else, only tell the invoice was created
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("The invoice has been created.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': invoice.user.pk}
|
||||
))
|
||||
|
||||
def can_use_payment(self, user, *_args, **_kwargs):
|
||||
"""Check if a user can use this payment.
|
||||
|
||||
Args:
|
||||
self: The payment
|
||||
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')
|
||||
or user.has_perm('cotisations.add_facture'),
|
||||
_("You cannot use this Payment.")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def find_allowed_payments(cls, user):
|
||||
"""Finds every allowed payments for an user.
|
||||
|
||||
Args:
|
||||
user: The user requesting payment methods.
|
||||
"""
|
||||
if user.has_perm('cotisations.use_every_payment'):
|
||||
return cls.objects.all()
|
||||
return cls.objects.filter(available_for_everyone=True)
|
||||
|
||||
def get_payment_method_name(self):
|
||||
p = find_payment_method(self)
|
||||
if p is not None:
|
||||
return p._meta.verbose_name
|
||||
return _("No custom payment method")
|
||||
|
||||
|
||||
class Cotisation(RevMixin, AclMixin, models.Model):
|
||||
|
|
136
cotisations/payment_methods/__init__.py
Normal file
136
cotisations/payment_methods/__init__.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
# -*- 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 = [
|
||||
comnpay,
|
||||
cheque,
|
||||
balance,
|
||||
]
|
27
cotisations/payment_methods/balance/__init__.py
Normal file
27
cotisations/payment_methods/balance/__init__.py
Normal file
|
@ -0,0 +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.
|
||||
"""
|
||||
This module contains a method to pay online using user balance.
|
||||
"""
|
||||
from . import models
|
||||
NAME = "BALANCE"
|
||||
|
||||
PaymentMethod = models.BalancePayment
|
120
cotisations/payment_methods/balance/models.py
Normal file
120
cotisations/payment_methods/balance/models.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
# -*- 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
|
||||
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.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _l("User Balance")
|
||||
|
||||
payment = models.OneToOneField(
|
||||
Paiement,
|
||||
on_delete=models.CASCADE,
|
||||
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,
|
||||
default=0,
|
||||
)
|
||||
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,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
credit_balance_allowed = models.BooleanField(
|
||||
verbose_name=_l("Allow user to credit their balance"),
|
||||
default=False,
|
||||
)
|
||||
|
||||
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:
|
||||
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
|
||||
)
|
||||
|
||||
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(
|
||||
'payment_method',
|
||||
_("There is already a payment type for user balance")
|
||||
)
|
||||
|
||||
def alter_payment(self, payment):
|
||||
"""Register the payment as a balance payment."""
|
||||
self.payment.is_balance = True
|
||||
|
||||
def check_price(self, price, user, *args, **kwargs):
|
||||
"""Checks that the price meets the requirement to be paid with user
|
||||
balance.
|
||||
"""
|
||||
return (
|
||||
float(user.solde) - float(price) >= self.minimum_balance,
|
||||
_("Your balance is too low for this operation.")
|
||||
)
|
||||
|
||||
def can_credit_balance(self, user_request):
|
||||
return (
|
||||
len(Paiement.find_allowed_payments(user_request)
|
||||
.exclude(is_balance=True)) > 0
|
||||
) and self.credit_balance_allowed
|
27
cotisations/payment_methods/cheque/__init__.py
Normal file
27
cotisations/payment_methods/cheque/__init__.py
Normal file
|
@ -0,0 +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.
|
||||
"""
|
||||
This module contains a method to pay online using cheque.
|
||||
"""
|
||||
from . import models, urls, views
|
||||
NAME = "CHEQUE"
|
||||
|
||||
PaymentMethod = models.ChequePayment
|
31
cotisations/payment_methods/cheque/forms.py
Normal file
31
cotisations/payment_methods/cheque/forms.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- 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 re2o.mixins import FormRevMixin
|
||||
from cotisations.models import Facture as Invoice
|
||||
|
||||
|
||||
class InvoiceForm(FormRevMixin, forms.ModelForm):
|
||||
"""A simple form to get the bank a the cheque number."""
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['banque', 'cheque']
|
54
cotisations/payment_methods/cheque/models.py
Normal file
54
cotisations/payment_methods/cheque/models.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- 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
|
||||
from django.utils.translation import ugettext_lazy as _l
|
||||
|
||||
from cotisations.models import Paiement
|
||||
from cotisations.payment_methods.mixins import PaymentMethodMixin
|
||||
|
||||
|
||||
class ChequePayment(PaymentMethodMixin, models.Model):
|
||||
"""
|
||||
The model allowing you to pay with a cheque.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _l("Cheque")
|
||||
|
||||
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(
|
||||
'cotisations:cheque:validate',
|
||||
kwargs={'invoice_pk': invoice.pk}
|
||||
))
|
30
cotisations/payment_methods/cheque/urls.py
Normal file
30
cotisations/payment_methods/cheque/urls.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# -*- 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
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^validate/(?P<invoice_pk>[0-9]+)$',
|
||||
views.cheque,
|
||||
name='validate'
|
||||
)
|
||||
]
|
69
cotisations/payment_methods/cheque/views.py
Normal file
69
cotisations/payment_methods/cheque/views.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
# -*- 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.
|
||||
"""
|
||||
|
||||
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 cotisations.utils import find_payment_method
|
||||
|
||||
from .models import ChequePayment
|
||||
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 = find_payment_method(invoice.paiement)
|
||||
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 = InvoiceForm(request.POST or None, instance=invoice)
|
||||
if form.is_valid():
|
||||
form.instance.valid = True
|
||||
form.save()
|
||||
return form.instance.paiement.end_payment(
|
||||
form.instance,
|
||||
request,
|
||||
use_payment_method=False
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
'cotisations/payment.html',
|
||||
{
|
||||
'form': form,
|
||||
'amount': invoice.prix_total()
|
||||
}
|
||||
)
|
26
cotisations/payment_methods/comnpay/__init__.py
Normal file
26
cotisations/payment_methods/comnpay/__init__.py
Normal file
|
@ -0,0 +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.
|
||||
"""
|
||||
This module contains a method to pay online using comnpay.
|
||||
"""
|
||||
from . import models, urls, views
|
||||
NAME = "COMNPAY"
|
||||
PaymentMethod = models.ComnpayPayment
|
|
@ -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
|
||||
"""
|
108
cotisations/payment_methods/comnpay/models.py
Normal file
108
cotisations/payment_methods/comnpay/models.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
# -*- 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
|
||||
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
|
||||
|
||||
from re2o.aes_field import AESEncryptedField
|
||||
from .comnpay import Transaction
|
||||
|
||||
|
||||
class ComnpayPayment(PaymentMethodMixin, models.Model):
|
||||
"""
|
||||
The model allowing you to pay with COMNPAY.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = "ComNpay"
|
||||
|
||||
payment = models.OneToOneField(
|
||||
Paiement,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='payment_method',
|
||||
editable=False
|
||||
)
|
||||
payment_credential = models.CharField(
|
||||
max_length=255,
|
||||
default='',
|
||||
blank=True,
|
||||
verbose_name=_l("ComNpay VAD Number"),
|
||||
)
|
||||
payment_pass = AESEncryptedField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_l("ComNpay Secret Key"),
|
||||
)
|
||||
minimum_payment = models.DecimalField(
|
||||
verbose_name=_l("Minimum payment"),
|
||||
help_text=_l("The minimal amount of money you have to use when paying"
|
||||
" with ComNpay"),
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
default=1,
|
||||
)
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
def check_price(self, price, *args, **kwargs):
|
||||
"""Checks that the price meets the requirement to be paid with ComNpay.
|
||||
"""
|
||||
return ((price >= self.minimum_payment),
|
||||
_('In order to pay your invoice with ComNpay'
|
||||
', the price must be grater than {} €')
|
||||
.format(self.minimum_payment))
|
40
cotisations/payment_methods/comnpay/urls.py
Normal file
40
cotisations/payment_methods/comnpay/urls.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# -*- 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
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^accept/(?P<factureid>[0-9]+)$',
|
||||
views.accept_payment,
|
||||
name='accept_payment'
|
||||
),
|
||||
url(
|
||||
r'^refuse/$',
|
||||
views.refuse_payment,
|
||||
name='refuse_payment'
|
||||
),
|
||||
url(
|
||||
r'^ipn/$',
|
||||
views.ipn,
|
||||
name='ipn'
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
@ -14,24 +34,38 @@ 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 .models import Facture
|
||||
from .payment_utils.comnpay import Payment as ComnpayPayment
|
||||
from cotisations.models import Facture
|
||||
from .comnpay import Transaction
|
||||
from .models import ComnpayPayment
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@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.
|
||||
"""
|
||||
facture = get_object_or_404(Facture, id=factureid)
|
||||
messages.success(
|
||||
request,
|
||||
_("The payment of %(amount)s € has been accepted.") % {
|
||||
'amount': facture.prix()
|
||||
}
|
||||
)
|
||||
invoice = get_object_or_404(Facture, id=factureid)
|
||||
if invoice.valid:
|
||||
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()):
|
||||
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}
|
||||
|
@ -42,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,
|
||||
|
@ -59,37 +94,38 @@ 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 = ComnpayPayment()
|
||||
p = Transaction()
|
||||
order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
|
||||
try:
|
||||
data = OrderedDict([(f, request.POST[f]) for f in order])
|
||||
except MultiValueDictKeyError:
|
||||
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
|
||||
|
||||
if not p.validSec(data, 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")
|
||||
|
@ -100,42 +136,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):
|
||||
"""
|
||||
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 = ComnpayPayment(
|
||||
str(AssoOption.get_cached_value('payment_id')),
|
||||
str(AssoOption.get_cached_value('payment_pass')),
|
||||
'https://' + host + reverse(
|
||||
'cotisations:accept_payment',
|
||||
kwargs={'factureid': facture.id}
|
||||
),
|
||||
'https://' + host + reverse('cotisations:refuse_payment'),
|
||||
'https://' + host + reverse('cotisations:ipn'),
|
||||
"",
|
||||
"D"
|
||||
)
|
||||
r = {
|
||||
'action': 'https://secure.homologation.comnpay.com',
|
||||
'method': 'POST',
|
||||
'content': p.buildSecretHTML(
|
||||
"Rechargement du solde",
|
||||
facture.prix(),
|
||||
idTransaction=str(facture.id)
|
||||
),
|
||||
'amount': facture.prix,
|
||||
}
|
||||
return r
|
||||
|
||||
|
||||
# The payment systems supported by re2o
|
||||
PAYMENT_SYSTEM = {
|
||||
'COMNPAY': comnpay,
|
||||
'NONE': None
|
||||
}
|
115
cotisations/payment_methods/forms.py
Normal file
115
cotisations/payment_methods/forms.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
# -*- 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
|
||||
|
||||
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 `None`.
|
||||
|
||||
Args:
|
||||
payment: The payment
|
||||
*args: arguments passed to the form
|
||||
creation: Should be True if you are creating the payment
|
||||
**kwargs: passed to the form
|
||||
|
||||
Returns:
|
||||
A form or None
|
||||
"""
|
||||
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
|
||||
)
|
||||
elif creation:
|
||||
return PaymentMethodForm(*args, **kwargs)
|
||||
|
||||
|
||||
class PaymentMethodForm(forms.Form):
|
||||
"""A special form which allows you to add a payment method to a `Payment`
|
||||
object.
|
||||
"""
|
||||
|
||||
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 the other "
|
||||
"options."
|
||||
),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentMethodForm, self).__init__(*args, **kwargs)
|
||||
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=='':
|
||||
return
|
||||
choice = int(choice)
|
||||
model = PAYMENT_METHODS[choice].PaymentMethod
|
||||
form = forms.modelform_factory(model, fields='__all__')(self.data, prefix=self.prefix)
|
||||
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):
|
||||
"""Saves the payment method.
|
||||
|
||||
Tries to call `payment_method.alter_payment` if it exists.
|
||||
"""
|
||||
commit = kwargs.pop('commit', True)
|
||||
if not hasattr(self, 'payment_method'):
|
||||
return None
|
||||
self.payment_method.payment = payment
|
||||
if hasattr(self.payment_method, 'alter_payment'):
|
||||
self.payment_method.alter_payment(payment)
|
||||
if commit:
|
||||
payment.save()
|
||||
self.payment_method.save()
|
||||
return self.payment_method
|
33
cotisations/payment_methods/mixins.py
Normal file
33
cotisations/payment_methods/mixins.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# -*- 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:
|
||||
"""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
|
||||
payment session if you whish.
|
||||
|
||||
Must return a HttpResponse-like object.
|
||||
"""
|
||||
return self.payment.end_payment(
|
||||
invoice, request, use_payment_method=False)
|
27
cotisations/payment_methods/urls.py
Normal file
27
cotisations/payment_methods/urls.py
Normal file
|
@ -0,0 +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.
|
||||
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')),
|
||||
]
|
|
@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<th>{% trans "Cotisation type" %}</th>
|
||||
<th>{% trans "Duration (month)" %}</th>
|
||||
<th>{% trans "Concerned users" %}</th>
|
||||
<th>{% trans "Available for everyone" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -42,7 +43,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ article.prix }}</td>
|
||||
<td>{{ article.type_cotisation }}</td>
|
||||
<td>{{ article.duration }}</td>
|
||||
<td>{{ article.type_user }}</td>
|
||||
<td>{{ article.type_user }}</td>
|
||||
<td>{{ article.available_for_everyone }}</td>
|
||||
<td class="text-right">
|
||||
{% can_edit article %}
|
||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}">
|
||||
|
|
|
@ -28,13 +28,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th>{% trans "Payment type" %}</th>
|
||||
<th>{% trans "Is available for everyone" %}</th>
|
||||
<th>{% trans "Custom payment method" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for paiement in paiement_list %}
|
||||
<tr>
|
||||
<td>{{ paiement.moyen }}</td>
|
||||
<td>{{ paiement.available_for_everyone }}</td>
|
||||
<td>
|
||||
{{paiement.get_payment_method_name}}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% can_edit paiement %}
|
||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}">
|
||||
|
|
|
@ -30,10 +30,27 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors factureform %}
|
||||
{% if title %}
|
||||
<h3>{{title}}</h3>
|
||||
{% else %}
|
||||
<h3>{% trans "New invoice" %}</h3>
|
||||
{% endif %}
|
||||
{% if max_balance %}
|
||||
<h4>{% trans "Maximum allowed balance : "%}{{max_balance}} €</h4>
|
||||
{% endif %}
|
||||
{% if balance is not None %}
|
||||
<p>
|
||||
{% trans "Current balance :" %} {{ balance }} €
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form factureform %}
|
||||
{% if payment_method %}
|
||||
{% bootstrap_form payment_method %}
|
||||
<div id="paymentMethod"></div>
|
||||
{% endif %}
|
||||
{% if articlesformset %}
|
||||
<h3>{% trans "Invoice's articles" %}</h3>
|
||||
<div id="form_set" class="form-group">
|
||||
|
@ -56,17 +73,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% bootstrap_form factureform %}
|
||||
{% bootstrap_button action_name button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
{% if articlesformset %}
|
||||
{% if articlesformset or payment_method%}
|
||||
<script type="text/javascript">
|
||||
{% if articlesformset %}
|
||||
var prices = {};
|
||||
{% for article in articles %}
|
||||
{% for article in articlelist %}
|
||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||
{% endfor %}
|
||||
|
||||
|
||||
var template = `Article :
|
||||
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
||||
|
||||
|
@ -134,6 +151,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
}
|
||||
update_price();
|
||||
});
|
||||
{% endif %}
|
||||
{% if payment_method.templates %}
|
||||
var TEMPLATES = [
|
||||
"",
|
||||
{% for t in payment_method.templates %}
|
||||
{% if t %}
|
||||
`{% bootstrap_form t %}`,
|
||||
{% else %}
|
||||
"",
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
function update_payment_method_form(){
|
||||
var method = document.getElementById('paymentMethodSelect').value;
|
||||
if(method==""){
|
||||
method=0;
|
||||
}
|
||||
else{
|
||||
method = Number(method);
|
||||
method += 1;
|
||||
}
|
||||
console.log(method);
|
||||
var html = TEMPLATES[method];
|
||||
|
||||
document.getElementById('paymentMethod').innerHTML = html;
|
||||
}
|
||||
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -1,164 +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 factureform %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "New invoice" %}</h3>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
User's balance : {{ user.solde }} €
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_form factureform %}
|
||||
{{ venteform.management_form }}
|
||||
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
|
||||
<h3>{% trans "Invoice's articles" %}</h3>
|
||||
<div id="form_set" class="form-group">
|
||||
{% for form in venteform.forms %}
|
||||
<div class='product_to_sell form-inline'>
|
||||
{% trans "Article" %} :
|
||||
{% bootstrap_form form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Total price : <span id="total_price">0,00</span> €
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% trans "Create" as tr_create %}
|
||||
{% bootstrap_button tr_create button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var prices = {};
|
||||
{% for article in articlelist %}
|
||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||
{% endfor %}
|
||||
|
||||
var template = `Article :
|
||||
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm"
|
||||
id="id_form-__prefix__-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>`
|
||||
|
||||
function add_article(){
|
||||
// Index start at 0 => new_index = number of items
|
||||
var new_index =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
||||
var new_article = document.createElement('div');
|
||||
new_article.className = 'product_to_sell form-inline';
|
||||
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
||||
document.getElementById('form_set').appendChild(new_article);
|
||||
add_listenner_for_id(new_index);
|
||||
}
|
||||
|
||||
function update_price(){
|
||||
var price = 0;
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
var article, article_price, quantity;
|
||||
for (i = 0; i < product_count; ++i){
|
||||
article = document.getElementById(
|
||||
'id_form-' + i.toString() + '-article').value;
|
||||
if (article == '') {
|
||||
continue;
|
||||
}
|
||||
article_price = prices[article];
|
||||
quantity = document.getElementById(
|
||||
'id_form-' + i.toString() + '-quantity').value;
|
||||
price += article_price * quantity;
|
||||
}
|
||||
document.getElementById('total_price').innerHTML =
|
||||
price.toFixed(2).toString().replace('.', ',');
|
||||
}
|
||||
|
||||
function add_listenner_for_id(i){
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("onkeypress", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-quantity')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article-remove')
|
||||
.addEventListener("click", function(event) {
|
||||
var article = event.target.parentNode;
|
||||
article.parentNode.removeChild(article);
|
||||
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||
update_price();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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")
|
||||
.addEventListener("click", add_article, true);
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
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();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "New invoice" %}</h3>
|
||||
{{ venteform.management_form }}
|
||||
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
|
||||
<h3>{% trans "Invoice's articles" %}</h3>
|
||||
<div id="form_set" class="form-group">
|
||||
{% for form in venteform.forms %}
|
||||
<div class='product_to_sell form-inline'>
|
||||
{% trans "Article" %} :
|
||||
{% bootstrap_form form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Total price : <span id="total_price">0,00</span> €
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var prices = {};
|
||||
{% for article in articlelist %}
|
||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||
{% endfor %}
|
||||
|
||||
var template = `Article :
|
||||
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm" id="id_form-__prefix__-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>`
|
||||
|
||||
function add_article(){
|
||||
// Index start at 0 => new_index = number of items
|
||||
var new_index =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
||||
var new_article = document.createElement('div');
|
||||
new_article.className = 'product_to_sell form-inline';
|
||||
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
||||
document.getElementById('form_set').appendChild(new_article);
|
||||
add_listenner_for_id(new_index);
|
||||
}
|
||||
|
||||
function update_price(){
|
||||
var price = 0;
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
var article, article_price, quantity;
|
||||
for (i = 0; i < product_count; ++i){
|
||||
article = document.getElementById(
|
||||
'id_form-' + i.toString() + '-article').value;
|
||||
if (article == '') {
|
||||
continue;
|
||||
}
|
||||
article_price = prices[article];
|
||||
quantity = document.getElementById(
|
||||
'id_form-' + i.toString() + '-quantity').value;
|
||||
price += article_price * quantity;
|
||||
}
|
||||
document.getElementById('total_price').innerHTML =
|
||||
price.toFixed(2).toString().replace('.', ',');
|
||||
}
|
||||
|
||||
function add_listenner_for_id(i){
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article')
|
||||
.addEventListener("onkeypress", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-quantity')
|
||||
.addEventListener("change", update_price, true);
|
||||
document.getElementById('id_form-' + i.toString() + '-article-remove')
|
||||
.addEventListener("click", function(event) {
|
||||
var article = event.target.parentNode;
|
||||
article.parentNode.removeChild(article);
|
||||
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||
update_price();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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")
|
||||
.addEventListener("click", add_article, true);
|
||||
var product_count =
|
||||
document.getElementsByClassName('product_to_sell').length;
|
||||
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();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -32,11 +32,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% block content %}
|
||||
<h3>
|
||||
{% blocktrans %}
|
||||
Refill of {{ amount }} €
|
||||
Pay {{ amount }} €
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<form class="form" method="{{ method }}" action="{{ action }}">
|
||||
<form class="form" method="{{ method | default:"post" }}" action="{{ action }}">
|
||||
{{ content | safe }}
|
||||
{% if form %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% endif %}
|
||||
{% trans "Pay" as tr_pay %}
|
||||
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
|
||||
</form>
|
||||
|
|
|
@ -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 %}
|
||||
<h2>{% trans "Balance refill" %}</h2>
|
||||
<h3>
|
||||
{% blocktrans %}
|
||||
Balance : <span class="label label-default">{{ request.user.solde }} €</span>
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form rechargeform %}
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -29,7 +29,7 @@ from django.conf.urls import url
|
|||
|
||||
import re2o
|
||||
from . import views
|
||||
from . import payment
|
||||
from . import payment_methods
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
|
@ -133,30 +133,5 @@ urlpatterns = [
|
|||
views.control,
|
||||
name='control'
|
||||
),
|
||||
url(
|
||||
r'^new_facture_solde/(?P<userid>[0-9]+)$',
|
||||
views.new_facture_solde,
|
||||
name='new_facture_solde'
|
||||
),
|
||||
url(
|
||||
r'^recharge/$',
|
||||
views.recharge,
|
||||
name='recharge'
|
||||
),
|
||||
url(
|
||||
r'^payment/accept/(?P<factureid>[0-9]+)$',
|
||||
payment.accept_payment,
|
||||
name='accept_payment'
|
||||
),
|
||||
url(
|
||||
r'^payment/refuse/$',
|
||||
payment.refuse_payment,
|
||||
name='refuse_payment'
|
||||
),
|
||||
url(
|
||||
r'^payment/ipn/$',
|
||||
payment.ipn,
|
||||
name='ipn'
|
||||
),
|
||||
url(r'^$', views.index, name='index'),
|
||||
]
|
||||
] + payment_methods.urls.urlpatterns
|
||||
|
|
32
cotisations/utils.py
Normal file
32
cotisations/utils.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# -*- 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:
|
||||
o = method.PaymentMethod.objects.get(payment=payment)
|
||||
return o
|
||||
except method.PaymentMethod.DoesNotExist:
|
||||
pass
|
||||
return None
|
21
cotisations/validators.py
Normal file
21
cotisations/validators.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from django.forms import ValidationError
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
def check_no_balance(is_balance):
|
||||
"""This functions checks that no Paiement with is_balance=True exists
|
||||
|
||||
Args:
|
||||
is_balance: True if the model is balance.
|
||||
|
||||
Raises:
|
||||
ValidationError: if such a Paiement exists.
|
||||
"""
|
||||
from .models import Paiement
|
||||
if not is_balance:
|
||||
return
|
||||
p = Paiement.objects.filter(is_balance=True)
|
||||
if len(p) > 0:
|
||||
raise ValidationError(
|
||||
_("There are already payment method(s) for user balance")
|
||||
)
|
|
@ -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
|
||||
|
@ -31,7 +32,7 @@ from __future__ import unicode_literals
|
|||
import os
|
||||
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.db.models import ProtectedError
|
||||
|
@ -56,11 +57,10 @@ 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,
|
||||
EditFactureForm,
|
||||
FactureForm,
|
||||
ArticleForm,
|
||||
DelArticleForm,
|
||||
PaiementForm,
|
||||
|
@ -70,11 +70,11 @@ from .forms import (
|
|||
NewFactureFormPdf,
|
||||
SelectUserArticleForm,
|
||||
SelectClubArticleForm,
|
||||
CreditSoldeForm,
|
||||
RechargeForm
|
||||
)
|
||||
from . import payment as online_payment
|
||||
from .tex import render_invoice
|
||||
from .payment_methods.forms import payment_method_factory
|
||||
from .utils import find_payment_method
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -84,29 +84,33 @@ def new_facture(request, user, userid):
|
|||
"""
|
||||
View called to create a new invoice.
|
||||
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 formset of a new invoice (based on the `:forms:FactureForm()` 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.
|
||||
"""
|
||||
invoice = Facture(user=user)
|
||||
# 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)
|
||||
)
|
||||
# Building the invocie form and the article formset
|
||||
invoice_form = NewFactureForm(request.POST or None, instance=invoice)
|
||||
# Building the invoice form and the article formset
|
||||
invoice_form = FactureForm(
|
||||
request.POST or None,
|
||||
instance=invoice,
|
||||
user=request.user,
|
||||
creation=True
|
||||
)
|
||||
|
||||
if request.user.is_class_club:
|
||||
article_formset = formset_factory(SelectClubArticleForm)(
|
||||
request.POST or None
|
||||
request.POST or None,
|
||||
form_kwargs={'user': request.user}
|
||||
)
|
||||
else:
|
||||
article_formset = formset_factory(SelectUserArticleForm)(
|
||||
request.POST or None
|
||||
request.POST or None,
|
||||
form_kwargs={'user': request.user}
|
||||
)
|
||||
|
||||
if invoice_form.is_valid() and article_formset.is_valid():
|
||||
|
@ -114,42 +118,15 @@ 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}
|
||||
))
|
||||
# Saving the invoice
|
||||
new_invoice_instance.save()
|
||||
|
||||
# Building a purchase for each article sold
|
||||
purchases = []
|
||||
total_price = 0
|
||||
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(
|
||||
total_price += article.prix*quantity
|
||||
new_purchase = Vente(
|
||||
facture=new_invoice_instance,
|
||||
name=article.name,
|
||||
prix=article.prix,
|
||||
|
@ -157,41 +134,42 @@ def new_facture(request, user, userid):
|
|||
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 \
|
||||
extended to %(end_date)s.") % {
|
||||
'member_name': user.pseudo,
|
||||
'end_date': user.end_adhesion()
|
||||
}
|
||||
)
|
||||
# Else, only tell the invoice was created
|
||||
purchases.append(new_purchase)
|
||||
p = find_payment_method(new_invoice_instance.paiement)
|
||||
if hasattr(p, 'check_price'):
|
||||
price_ok, msg = p.check_price(total_price, user)
|
||||
invoice_form.add_error(None, msg)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("The invoice has been created.")
|
||||
price_ok = True
|
||||
if price_ok:
|
||||
new_invoice_instance.save()
|
||||
for p in purchases:
|
||||
p.facture = new_invoice_instance
|
||||
p.save()
|
||||
|
||||
return new_invoice_instance.paiement.end_payment(
|
||||
new_invoice_instance,
|
||||
request
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': userid}
|
||||
))
|
||||
messages.error(
|
||||
request,
|
||||
_("You need to choose at least one article.")
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
request,
|
||||
_("You need to choose at least one article.")
|
||||
)
|
||||
p = Paiement.objects.filter(is_balance=True)
|
||||
if len(p) and p[0].can_use_payment(request.user):
|
||||
balance = user.solde
|
||||
else:
|
||||
balance = None
|
||||
return form(
|
||||
{
|
||||
'factureform': invoice_form,
|
||||
'venteform': article_formset,
|
||||
'articlelist': article_list
|
||||
'articlesformset': article_formset,
|
||||
'articlelist': article_list,
|
||||
'balance': balance,
|
||||
'action_name': _('Create'),
|
||||
},
|
||||
'cotisations/new_facture.html', request
|
||||
'cotisations/facture.html', request
|
||||
)
|
||||
|
||||
|
||||
|
@ -212,11 +190,13 @@ def new_facture_pdf(request):
|
|||
invoice_form = NewFactureFormPdf(request.POST or None)
|
||||
if request.user.is_class_club:
|
||||
articles_formset = formset_factory(SelectClubArticleForm)(
|
||||
request.POST or None
|
||||
request.POST or None,
|
||||
form_kwargs={'user': request.user}
|
||||
)
|
||||
else:
|
||||
articles_formset = formset_factory(SelectUserArticleForm)(
|
||||
request.POST or None
|
||||
request.POST or None,
|
||||
form_kwargs={'user': request.user}
|
||||
)
|
||||
if invoice_form.is_valid() and articles_formset.is_valid():
|
||||
# Get the article list and build an list out of it
|
||||
|
@ -251,13 +231,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
|
||||
|
@ -300,7 +280,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
|
||||
|
@ -313,7 +293,7 @@ def edit_facture(request, facture, **_kwargs):
|
|||
can be set as desired. This is also the view used to invalidate
|
||||
an invoice.
|
||||
"""
|
||||
invoice_form = EditFactureForm(
|
||||
invoice_form = FactureForm(
|
||||
request.POST or None,
|
||||
instance=facture,
|
||||
user=request.user
|
||||
|
@ -324,7 +304,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
|
||||
|
@ -341,7 +321,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
|
||||
|
@ -361,40 +341,7 @@ def del_facture(request, facture, **_kwargs):
|
|||
return form({
|
||||
'objet': facture,
|
||||
'objet_name': _("Invoice")
|
||||
}, '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)
|
||||
}, 'cotisations/delete.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -419,8 +366,9 @@ def add_article(request):
|
|||
return redirect(reverse('cotisations:index-article'))
|
||||
return form({
|
||||
'factureform': article,
|
||||
'action_name': _("Add")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _("Add"),
|
||||
'title': _("New article")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -440,8 +388,9 @@ def edit_article(request, article_instance, **_kwargs):
|
|||
return redirect(reverse('cotisations:index-article'))
|
||||
return form({
|
||||
'factureform': article,
|
||||
'action_name': _('Edit')
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _('Edit'),
|
||||
'title': _("Edit article")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -461,8 +410,9 @@ def del_article(request, instances):
|
|||
return redirect(reverse('cotisations:index-article'))
|
||||
return form({
|
||||
'factureform': article,
|
||||
'action_name': _("Delete")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _("Delete"),
|
||||
'title': _("Delete article")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change paiement to payment
|
||||
|
@ -472,9 +422,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)
|
||||
messages.success(
|
||||
request,
|
||||
_("The payment method has been successfully created.")
|
||||
|
@ -482,8 +438,10 @@ def add_paiement(request):
|
|||
return redirect(reverse('cotisations:index-paiement'))
|
||||
return form({
|
||||
'factureform': payment,
|
||||
'action_name': _("Add")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'payment_method': payment_method,
|
||||
'action_name': _("Add"),
|
||||
'title': _("New payment method")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : chnage paiement to Payment
|
||||
|
@ -493,19 +451,34 @@ 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',
|
||||
creation=False
|
||||
)
|
||||
|
||||
if payment.is_valid() and \
|
||||
(payment_method is None or payment_method.is_valid()):
|
||||
payment.save()
|
||||
if payment_method is not None:
|
||||
payment_method.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("The payement method has been successfully edited.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-paiement'))
|
||||
return form({
|
||||
'factureform': payment,
|
||||
'action_name': _("Edit")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'payment_method': payment_method,
|
||||
'action_name': _("Edit"),
|
||||
'title': _("Edit payment method")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change paiement to payment
|
||||
|
@ -539,8 +512,9 @@ def del_paiement(request, instances):
|
|||
return redirect(reverse('cotisations:index-paiement'))
|
||||
return form({
|
||||
'factureform': payment,
|
||||
'action_name': _("Delete")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _("Delete"),
|
||||
'title': _("Delete payment method")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
|
@ -560,8 +534,9 @@ def add_banque(request):
|
|||
return redirect(reverse('cotisations:index-banque'))
|
||||
return form({
|
||||
'factureform': bank,
|
||||
'action_name': _("Add")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _("Add"),
|
||||
'title': _("New bank")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
|
@ -582,8 +557,9 @@ def edit_banque(request, banque_instance, **_kwargs):
|
|||
return redirect(reverse('cotisations:index-banque'))
|
||||
return form({
|
||||
'factureform': bank,
|
||||
'action_name': _("Edit")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _("Edit"),
|
||||
'title': _("Edit bank")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : chnage banque to bank
|
||||
|
@ -617,8 +593,9 @@ def del_banque(request, instances):
|
|||
return redirect(reverse('cotisations:index-banque'))
|
||||
return form({
|
||||
'factureform': bank,
|
||||
'action_name': _("Delete")
|
||||
}, 'cotisations/facture.html', request)
|
||||
'action_name': _("Delete"),
|
||||
'title': _("Delete bank")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
|
@ -659,7 +636,7 @@ def control(request):
|
|||
return render(request, 'cotisations/control.html', {
|
||||
'facture_list': invoice_list,
|
||||
'controlform': control_invoices_form
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -672,7 +649,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
|
||||
|
@ -685,7 +662,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
|
||||
|
@ -698,7 +675,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
|
||||
|
@ -719,156 +696,62 @@ 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
|
||||
# TODO : change facture to invoice
|
||||
# TODO : change solde to balance
|
||||
@login_required
|
||||
def new_facture_solde(request, userid):
|
||||
@can_edit(User)
|
||||
def credit_solde(request, user, **_kwargs):
|
||||
"""
|
||||
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.
|
||||
View used to edit the balance of a user.
|
||||
Can be use either to increase or decrease a user's balance.
|
||||
"""
|
||||
user = request.user
|
||||
invoice = Facture(user=user)
|
||||
payment, _created = Paiement.objects.get_or_create(moyen='Solde')
|
||||
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
|
||||
)
|
||||
try:
|
||||
balance = find_payment_method(Paiement.objects.get(is_balance=True))
|
||||
except Paiement.DoesNotExist:
|
||||
credit_allowed = False
|
||||
else:
|
||||
article_formset = formset_factory(SelectUserArticleForm)(
|
||||
request.POST or None
|
||||
credit_allowed = (
|
||||
balance is not None
|
||||
and balance.can_credit_balance(request.user)
|
||||
)
|
||||
|
||||
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}
|
||||
))
|
||||
if not credit_allowed:
|
||||
messages.error(
|
||||
request,
|
||||
_("You need to choose at least one article.")
|
||||
_("You are not allowed to credit your balance.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': userid}
|
||||
kwargs={'userid': user.id}
|
||||
))
|
||||
|
||||
return form({
|
||||
'venteform': article_formset,
|
||||
'articlelist': article_list
|
||||
}, 'cotisations/new_facture_solde.html', request)
|
||||
|
||||
|
||||
# TODO : change recharge to refill
|
||||
@login_required
|
||||
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.valid = False
|
||||
invoice.save()
|
||||
purchase = Vente.objects.create(
|
||||
facture=invoice,
|
||||
name='solde',
|
||||
prix=refill_form.cleaned_data['value'],
|
||||
number=1
|
||||
)
|
||||
purchase.save()
|
||||
content = online_payment.PAYMENT_SYSTEM[
|
||||
AssoOption.get_cached_value('payment')
|
||||
](invoice, request)
|
||||
return render(request, 'cotisations/payment.html', content)
|
||||
price = refill_form.cleaned_data['value']
|
||||
invoice = Facture(user=user)
|
||||
invoice.paiement = refill_form.cleaned_data['payment']
|
||||
p = find_payment_method(invoice.paiement)
|
||||
if hasattr(p, 'check_price'):
|
||||
price_ok, msg = p.check_price(price, user)
|
||||
refill_form.add_error(None, msg)
|
||||
else:
|
||||
price_ok = True
|
||||
if price_ok:
|
||||
invoice.valid = True
|
||||
invoice.save()
|
||||
Vente.objects.create(
|
||||
facture=invoice,
|
||||
name='solde',
|
||||
prix=refill_form.cleaned_data['value'],
|
||||
number=1
|
||||
)
|
||||
return invoice.paiement.end_payment(invoice, request)
|
||||
p = get_object_or_404(Paiement, is_balance=True)
|
||||
return form({
|
||||
'rechargeform': refill_form
|
||||
}, 'cotisations/recharge.html', request)
|
||||
'factureform': refill_form,
|
||||
'balance': request.user.solde,
|
||||
'title': _("Refill your balance"),
|
||||
'action_name': _("Pay"),
|
||||
'max_balance': p.payment_method.maximum_balance,
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
|
|
@ -38,6 +38,7 @@ from .models import (
|
|||
Service
|
||||
)
|
||||
|
||||
|
||||
class EditOptionalUserForm(ModelForm):
|
||||
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
|
||||
class Meta:
|
||||
|
@ -54,13 +55,6 @@ class EditOptionalUserForm(ModelForm):
|
|||
self.fields['is_tel_mandatory'].label = (
|
||||
'Exiger un numéro de téléphone'
|
||||
)
|
||||
self.fields['user_solde'].label = (
|
||||
'Activation du solde pour les utilisateurs'
|
||||
)
|
||||
self.fields['max_solde'].label = 'Solde maximum'
|
||||
self.fields['min_online_payment'].label = (
|
||||
'Montant de rechargement minimum en ligne'
|
||||
)
|
||||
self.fields['self_adhesion'].label = 'Auto inscription'
|
||||
|
||||
|
||||
|
|
|
@ -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."),
|
||||
),
|
||||
]
|
|
@ -3,7 +3,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import preferences.aes_field
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -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 re2o.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',
|
||||
|
|
20
preferences/migrations/0044_remove_payment_pass.py
Normal file
20
preferences/migrations/0044_remove_payment_pass.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- 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_pass',
|
||||
),
|
||||
]
|
||||
|
44
preferences/migrations/0045_remove_unused_payment_fields.py
Normal file
44
preferences/migrations/0045_remove_unused_payment_fields.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# -*- 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', '0044_remove_payment_pass'),
|
||||
('cotisations', '0030_custom_payment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assooption',
|
||||
name='payment',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assooption',
|
||||
name='payment_id',
|
||||
),
|
||||
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',
|
||||
),
|
||||
]
|
|
@ -31,12 +31,9 @@ from django.db.models.signals import post_save
|
|||
from django.dispatch import receiver
|
||||
from django.core.cache import cache
|
||||
|
||||
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 +64,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,
|
||||
|
@ -108,14 +89,6 @@ class OptionalUser(AclMixin, PreferencesModel):
|
|||
("view_optionaluser", "Peut voir les options de l'user"),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@receiver(post_save, sender=OptionalUser)
|
||||
def optionaluser_post_save(**kwargs):
|
||||
|
@ -294,25 +267,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,
|
||||
|
|
|
@ -31,46 +31,30 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% block content %}
|
||||
<h4>Préférences utilisateur</h4>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
<i class="fa fa-edit"></i>
|
||||
Editer
|
||||
</a>
|
||||
</a>
|
||||
<p>
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Téléphone obligatoirement requis</th>
|
||||
<td>{{ useroptions.is_tel_mandatory }}</td>
|
||||
<th>Activation du solde pour les utilisateurs</th>
|
||||
<td>{{ useroptions.user_solde }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Champ gpg fingerprint</th>
|
||||
<td>{{ useroptions.gpg_fingerprint }}</td>
|
||||
{% if useroptions.user_solde %}
|
||||
<th>Solde négatif</th>
|
||||
<td>{{ useroptions.solde_negatif }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creations d'adhérents par tous</th>
|
||||
<td>{{ useroptions.all_can_create_adherent }}</td>
|
||||
<th>Creations de clubs par tous</th>
|
||||
<td>{{ useroptions.all_can_create_club }}</td>
|
||||
<th>Creations de clubs par tous</th>
|
||||
<td>{{ useroptions.all_can_create_club }}</td>
|
||||
</tr>
|
||||
{% if useroptions.user_solde %}
|
||||
<tr>
|
||||
<th>Solde maximum</th>
|
||||
<td>{{ useroptions.max_solde }}</td>
|
||||
<th>Montant minimal de rechargement en ligne</th>
|
||||
<td>{{ useroptions.min_online_payment }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Auto inscription</th>
|
||||
<th>Auto inscription</th>
|
||||
<td>{{ useroptions.self_adhesion }}</td>
|
||||
<th>Shell par défaut des utilisateurs</th>
|
||||
<td>{{ useroptions.shell_default }}</td>
|
||||
</tr>
|
||||
<th>Shell par défaut des utilisateurs</th>
|
||||
<td>{{ useroptions.shell_default }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>Préférences machines</h4>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
|
||||
|
@ -91,11 +75,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ machineoptions.max_lambdauser_aliases }}</td>
|
||||
<th>Support de l'ipv6</th>
|
||||
<td>{{ machineoptions.ipv6_mode }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creation de machines</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creation de machines</th>
|
||||
<td>{{ machineoptions.create_machine }}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>Préférences topologie</h4>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
|
||||
|
@ -108,7 +92,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th>Politique générale de placement de vlan</th>
|
||||
<td>{{ topologieoptions.radius_general_policy }}</td>
|
||||
<th> 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"</th>
|
||||
<th> 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"</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -144,12 +128,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<th>Temps avant expiration du lien de reinitialisation de mot de passe (en heures)</th>
|
||||
<td>{{ generaloptions.req_expire_hrs }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr>
|
||||
<th>Message global affiché sur le site</th>
|
||||
<td>{{ generaloptions.general_message }}</td>
|
||||
<th>Résumé des CGU</th>
|
||||
<td>{{ generaloptions.GTU_sum_up }}</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<tr>
|
||||
<th>CGU</th>
|
||||
<td>{{generaloptions.GTU}}</th>
|
||||
|
@ -171,8 +155,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</tr>
|
||||
<tr>
|
||||
<th>Adresse</th>
|
||||
<td>{{ assooptions.adresse1 }}<br>
|
||||
{{ assooptions.adresse2 }}</td>
|
||||
<td>{{ assooptions.adresse1 }}<br>
|
||||
{{ assooptions.adresse2 }}</td>
|
||||
<th>Contact mail</th>
|
||||
<td>{{ assooptions.contact }}</td>
|
||||
</tr>
|
||||
|
@ -185,13 +169,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th>Objet utilisateur de l'association</th>
|
||||
<td>{{ assooptions.utilisateur_asso }}</td>
|
||||
<th>Moyen de paiement automatique</th>
|
||||
<td>{{ assooptions.payment }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description de l'association</th>
|
||||
<td colspan="3">{{ assooptions.description | safe }}</td>
|
||||
</tr>
|
||||
<th>Description de l'association</th>
|
||||
<td>{{ assooptions.description | safe }}</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<h4>Messages personalisé dans les mails</h4>
|
||||
|
@ -205,7 +185,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th>Mail de bienvenue (Français)</th>
|
||||
<td>{{ mailmessageoptions.welcome_mail_fr | safe }}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Mail de bienvenue (Anglais)</th>
|
||||
<td>{{ mailmessageoptions.welcome_mail_en | safe }}</td>
|
||||
|
|
|
@ -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,18 +65,35 @@ 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 """
|
||||
|
||||
def save_form_data(self, instance, data):
|
||||
setattr(instance, self.name,
|
||||
binascii.b2a_base64(encrypt(settings.AES_KEY, data)))
|
||||
setattr(instance, self.name, binascii.b2a_base64(
|
||||
encrypt(settings.AES_KEY, data)).decode('utf-8'))
|
||||
|
||||
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:
|
||||
raise ValueError(value)
|
||||
|
||||
def from_db_value(self, value, *args, **kwargs):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
return decrypt(settings.AES_KEY,
|
||||
binascii.a2b_base64(value)).decode('utf-8')
|
||||
except Exception as e:
|
||||
raise ValueError(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
|
@ -85,4 +101,9 @@ class AESEncryptedField(models.CharField):
|
|||
return binascii.b2a_base64(encrypt(
|
||||
settings.AES_KEY,
|
||||
value
|
||||
))
|
||||
)).decode('utf-8')
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': AESEncryptedFormField}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
|
@ -10,6 +10,7 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
('topologie', '0029_auto_20171002_0334'),
|
||||
('machines', '0049_vlan'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
@ -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(is_balance=True)
|
||||
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 à
|
||||
|
|
|
@ -33,13 +33,11 @@ un {{ users.class_name | lower}}</span>{% else %}<span class="label label-danger
|
|||
non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
|
||||
<span class="label label-success">active</span>{% else %}<span class="label label-danger">désactivée</span>{% endif %}.</p>
|
||||
{% if user_solde %}
|
||||
<p>Votre solde est de <span class="badge">{{ user.solde }}€</span>.
|
||||
{% if allow_online_payment %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:recharge' %}">
|
||||
<p>Votre solde est de <span class="badge">{{ users.solde }}€</span>.
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:credit-solde' users.pk%}">
|
||||
<i class="fa fa-euro-sign"></i>
|
||||
Recharger
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
@ -166,8 +164,8 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
|
|||
<tr>
|
||||
<th>Solde</th>
|
||||
<td>{{ users.solde }} €
|
||||
{% if allow_online_payment %}
|
||||
<a class="btn btn-primary btn-sm" style='float:right' role="button" href="{% url 'cotisations:recharge' %}">
|
||||
{% if user_solde %}
|
||||
<a class="btn btn-primary btn-sm" style='float:right' role="button" href="{% url 'cotisations:credit-solde' users.pk%}">
|
||||
<i class="fa fa-euro-sign"></i>
|
||||
Recharger
|
||||
</a>
|
||||
|
@ -283,12 +281,8 @@ non adhérent</span>{% endif %} et votre connexion est {% if users.has_access %}
|
|||
<i class="fa fa-euro-sign"></i>
|
||||
Modifier le solde
|
||||
</a>
|
||||
{% endif%}{% acl_else %}
|
||||
{% if user_solde %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:new_facture_solde' user.id %}">
|
||||
<i class="fa fa-euro-sign"></i>
|
||||
Ajouter une cotisation par solde</a>{% endif %}{% acl_end %}
|
||||
</a>
|
||||
{% endif%}
|
||||
{% acl_end %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if facture_list %}
|
||||
|
|
|
@ -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,
|
||||
|
@ -67,6 +67,7 @@ from re2o.acl import (
|
|||
can_view_all,
|
||||
can_change
|
||||
)
|
||||
from cotisations.utils import find_payment_method
|
||||
|
||||
from .serializers import MailingSerializer, MailingMemberSerializer
|
||||
from .models import (
|
||||
|
@ -246,7 +247,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 +406,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 +483,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)
|
||||
|
@ -852,7 +855,7 @@ def mon_profil(request):
|
|||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': str(request.user.id)}
|
||||
))
|
||||
))
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -896,20 +899,26 @@ def profil(request, users, **_kwargs):
|
|||
request.GET.get('order'),
|
||||
SortTable.USERS_INDEX_WHITE
|
||||
)
|
||||
user_solde = OptionalUser.get_cached_value('user_solde')
|
||||
allow_online_payment = AssoOption.get_cached_value('payment') != 'NONE'
|
||||
try:
|
||||
balance = find_payment_method(Paiement.objects.get(is_balance=True))
|
||||
except Paiement.DoesNotExist:
|
||||
user_solde = False
|
||||
else:
|
||||
user_solde = (
|
||||
balance is not None
|
||||
and balance.can_credit_balance(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,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -974,6 +983,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'
|
||||
|
|
Loading…
Reference in a new issue