Le serveur gitlab sera inaccessible le mercredi 19 février 2020 de 13h à 14h pour une intervention de maintenance programmée.

...
 
Commits (175)
......@@ -94,3 +94,14 @@ If you to restrict the IP which can see the debug, use the `INTERNAL_IPS` option
```
INTERNAL_IPS = ["10.0.0.1", "10.0.0.2"]
```
## MR 174 : Fix online payment + allow users to pay their subscription
Add the possibility to use custom payment methods. There is also a boolean field on the
Payments allowing every user to use some kinds of payment. You have to add the rights `cotisations.use_every_payment` and `cotisations.buy_every_article`
to the staff members so they can use every type of payment to buy anything.
Don't forget to run migrations, several settings previously in the `preferences` app ar now
in their own Payment models.
To have a closer look on how the payments works, pleas go to the wiki.
# -*- 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.
......
# -*- 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.
......
# -*- 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.
......
# -*- 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.
......
# -*- 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.
......
This diff is collapsed.
# -*- 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.
......
# -*- 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.
......
# -*- 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.
......@@ -51,6 +52,7 @@ router.register_viewset(r'machines/extension', views.ExtensionViewSet)
router.register_viewset(r'machines/mx', views.MxViewSet)
router.register_viewset(r'machines/ns', views.NsViewSet)
router.register_viewset(r'machines/txt', views.TxtViewSet)
router.register_viewset(r'machines/dname', views.DNameViewSet)
router.register_viewset(r'machines/srv', views.SrvViewSet)
router.register_viewset(r'machines/interface', views.InterfaceViewSet)
router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet)
......@@ -60,6 +62,7 @@ router.register_viewset(r'machines/service', views.ServiceViewSet)
router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink')
router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet)
router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet)
router.register_viewset(r'machines/role', views.RoleViewSet)
# PREFERENCES
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
......@@ -79,6 +82,7 @@ router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchV
router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet)
router.register_viewset(r'topologie/building', views.BuildingViewSet)
router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport')
router.register_viewset(r'topologie/portprofile', views.PortProfileViewSet, base_name='portprofile')
router.register_viewset(r'topologie/room', views.RoomViewSet)
# USERS
router.register_viewset(r'users/user', views.UserViewSet)
......@@ -90,10 +94,18 @@ router.register_viewset(r'users/listright', views.ListRightViewSet)
router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell')
router.register_viewset(r'users/ban', views.BanViewSet)
router.register_viewset(r'users/whitelist', views.WhitelistViewSet)
router.register_viewset(r'users/mailalias', views.MailAliasViewSet)
# SERVICE REGEN
router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen')
# DHCP
router.register_view(r'dhcp/hostmacip', views.HostMacIpView),
# Switches config
router.register_view(r'switchs/ports-config', views.SwitchPortView),
router.register_view(r'switchs/role', views.RoleView),
# Mail config
router.register_view(r'mail/alias', views.UserMailAliasView),
# Reminder
router.register_view(r'reminder/get-users', views.ReminderView),
# DNS
router.register_view(r'dns/zones', views.DNSZonesView),
# MAILING
......
# -*- 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.
......@@ -162,6 +163,12 @@ class TxtViewSet(viewsets.ReadOnlyModelViewSet):
queryset = machines.Txt.objects.all()
serializer_class = serializers.TxtSerializer
class DNameViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.DName` objects.
"""
queryset = machines.DName.objects.all()
serializer_class = serializers.DNameSerializer
class SrvViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.Srv` objects.
......@@ -227,6 +234,13 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.OuverturePortSerializer
class RoleViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.Machine` objects.
"""
queryset = machines.Role.objects.all()
serializer_class = serializers.RoleSerializer
# PREFERENCES
# Those views differ a bit because there is only one object
# to display, so we don't bother with the listing part
......@@ -382,6 +396,13 @@ class SwitchPortViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.SwitchPortSerializer
class PortProfileViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.PortProfile` objects.
"""
queryset = topologie.PortProfile.objects.all()
serializer_class = serializers.PortProfileSerializer
class RoomViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.Room` objects.
"""
......@@ -455,6 +476,13 @@ class WhitelistViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.WhitelistSerializer
class MailAliasViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.MailAlias` objects.
"""
queryset = users.MailAlias.objects.all()
serializer_class = serializers.MailAliasSerializer
# SERVICE REGEN
......@@ -474,6 +502,45 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
queryset = queryset.filter(server__domain__name__iexact=hostname)
return queryset
# Config des switches
class SwitchPortView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
serializer_class = serializers.SwitchPortSerializer
class RoleView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = machines.Role.objects.all().prefetch_related('servers')
serializer_class = serializers.RoleSerializer
# Server mail config
class UserMailAliasView(generics.ListAPIView):
"""Expose all the aliases of the users that activated the internal address
"""
queryset = users.User.objects.filter(internal_address=True)
serializer_class = serializers.UserMailAliasSerializer
# Rappel fin adhésion
class ReminderView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = preferences.Reminder.objects.all()
serializer_class = serializers.ReminderSerializer
# DHCP
......
This diff is collapsed.
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-17 14:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0029_auto_20180414_2056'),
]
operations = [
migrations.AddField(
model_name='paiement',
name='allow_self_subscription',
field=models.BooleanField(default=False, verbose_name='Is available for self subscription'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-17 17:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0030_paiement_allow_self_subscription'),
]
operations = [
migrations.AddField(
model_name='article',
name='allow_self_subscription',
field=models.BooleanField(default=False, verbose_name='Is available for self subscription'),
),
]
# -*- 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()
payment, _created = Payment.objects.get_or_create(
moyen='Rechargement en ligne'
)
comnpay = ComnpayPayment()
comnpay.payment_user = options.payment_id
comnpay.payment = payment
comnpay.save()
payment.moyen = "ComnPay"
payment.save()
class Migration(migrations.Migration):
dependencies = [
('preferences', '0044_remove_payment_pass'),
('cotisations', '0031_article_allow_self_subscription'),
]
operations = [
migrations.CreateModel(
name='ChequePayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
),
migrations.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)),
('payment_pass', re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True)),
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
),
migrations.RunPython(add_comnpay),
migrations.RunPython(add_cheque),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-03 13:53
from __future__ import unicode_literals
import cotisations.payment_methods.mixins
from django.db import migrations, models
import django.db.models.deletion
def add_solde(apps, schema_editor):
OptionalUser = apps.get_model('preferences', 'OptionalUser')
options, _created = OptionalUser.objects.get_or_create()
Payment = apps.get_model('cotisations', 'Paiement')
BalancePayment = apps.get_model('cotisations', 'BalancePayment')
solde, _created = Payment.objects.get_or_create(moyen="solde")
balance = BalancePayment()
balance.payment = solde
balance.minimum_balance = options.solde_negatif
balance.save()
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0032_chequepayment_comnpaypayment'),
]
operations = [
migrations.CreateModel(
name='BalancePayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('minimum_balance', models.DecimalField(decimal_places=2, help_text='The minimal amount of money allowed for the balance at the end of a payment. You can specify negative amount.', max_digits=5, verbose_name='Minimum balance')),
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
),
migrations.RunPython(add_solde)
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-03 14:29
from __future__ import unicode_literals
import re2o.aes_field
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0033_balancepayment'),
]
operations = [
migrations.AlterField(
model_name='comnpaypayment',
name='payment_credential',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='ComNpay VAD Number'),
),
migrations.AlterField(
model_name='comnpaypayment',
name='payment_pass',
field=re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay Secret Key'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-03 15:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0034_auto_20180703_0929'),
]
operations = [
migrations.AlterModelOptions(
name='paiement',
options={'permissions': (('view_paiement', "Can see a payement's details"), ('use', 'Can use a payement')), 'verbose_name': 'Payment method', 'verbose_name_plural': 'Payment methods'},
),
migrations.RemoveField(
model_name='paiement',
name='allow_self_subscription',
),
migrations.AddField(
model_name='paiement',
name='available_for_everyone',
field=models.BooleanField(default=False, verbose_name='Is available for every user'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-03 15:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0035_auto_20180703_1005'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'permissions': (('view_article', "Can see an article's details"), ('buy_every_article', 'Can buy every_article')), 'verbose_name': 'Article', 'verbose_name_plural': 'Articles'},
),
migrations.RemoveField(
model_name='article',
name='allow_self_subscription',
),
migrations.AddField(
model_name='article',
name='available_for_everyone',
field=models.BooleanField(default=False, verbose_name='Is available for every user'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-03 17:02
from __future__ import unicode_literals
from django.db import migrations, models
def create_max_balance(apps, schema_editor):
OptionalUser = apps.get_model('preferences', 'OptionalUser')
Payment = apps.get_model('cotisations', 'Paiement')
balance, _created = Payment.objects.get_or_create(moyen='solde')
options, _created = OptionalUser.objects.get_or_create()
balance.maximum_balance = options.max_solde
balance.save()
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0036_auto_20180703_1056'),
]
operations = [
migrations.AlterModelOptions(
name='paiement',
options={'permissions': (('view_paiement', "Can see a payement's details"), ('use_every_payment', 'Can use every payement')), 'verbose_name': 'Payment method', 'verbose_name_plural': 'Payment methods'},
),
migrations.AddField(
model_name='balancepayment',
name='maximum_balance',
field=models.DecimalField(decimal_places=2, default=50, help_text='The maximal amount of money allowed for the balance.', max_digits=5, verbose_name='Maximum balance'),
),
migrations.RunPython(create_max_balance)
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-04 16:30
from __future__ import unicode_literals
from django.db import migrations, models
def update_balance(apps, _):
Payment = apps.get_model('cotisations', 'Paiement')
try:
balance = Payment.objects.get(moyen="solde")
balance.is_balance = True
balance.save()
except Payment.DoesNotExist:
pass
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0037_auto_20180703_1202'),
]
operations = [
migrations.AddField(
model_name='paiement',
name='is_balance',
field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', verbose_name='Is user balance'),
),
migrations.RunPython(update_balance)
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-04 16:47
from __future__ import unicode_literals
import cotisations.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0038_paiement_is_balance'),
]
operations = [
migrations.AlterField(
model_name='paiement',
name='is_balance',
field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', validators=[cotisations.models.check_no_balance], verbose_name='Is user balance'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-05 13:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0039_auto_20180704_1147'),
]
operations = [
migrations.AlterField(
model_name='balancepayment',
name='minimum_balance',
field=models.DecimalField(decimal_places=2, default=0, help_text='The minimal amount of money allowed for the balance at the end of a payment. You can specify negative amount.', max_digits=5, verbose_name='Minimum balance'),
),
]
......@@ -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,16 @@ 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
# TODO : change facture to invoice
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
......@@ -131,7 +137,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 +149,12 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
# TODO : change Vente to somethingelse
return Vente.objects.filter(
facture=self
).aggregate(
total=models.Sum(
models.F('prix')*models.F('number'),
output_field=models.FloatField()
)
)['total']
).aggregate(
total=models.Sum(
models.F('prix')*models.F('number'),
output_field=models.FloatField()
)
)['total']
def name(self):
"""
......@@ -157,7 +163,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 +219,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.
"""
nb_payments = len(Paiement.find_allowed_payments(user_request))
nb_articles = len(Article.find_allowed_articles(user_request))
return (
user_request.has_perm('cotisations.add_facture')
or (nb_payments*nb_articles),
_("You don't have the right to create an invoice.")
)
def __init__(self, *args, **kwargs):
super(Facture, self).__init__(*args, **kwargs)
self.field_permissions = {
......@@ -341,12 +363,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 +379,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 +402,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 +523,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 +551,24 @@ class Article(RevMixin, AclMixin, models.Model):
def __str__(self):
return self.name
def can_buy_article(self, user, *_args, **_kwargs):
"""Check if a user can buy this article.
:param self: The article
:param user: The user requesting buying
:returns: A boolean stating if usage is granted and an explanation
message if the boolean is `False`.
"""
return (
self.available_for_everyone
or user.has_perm('cotisations.buy_every_article'),
_("You cannot use this Payment.")
)
@classmethod
def find_allowed_articles(cls, user):
return [p for p in cls.objects.all() if p.can_buy_article(user)[0]]
class Banque(RevMixin, AclMixin, models.Model):
"""
......@@ -550,6 +595,17 @@ class Banque(RevMixin, AclMixin, models.Model):
return self.name
def check_no_balance():
"""This functions checks that no Paiement with is_balance=True exists
:raises ValidationError: if such a Paiement exists.
"""
p = Paiement.objects.filter(is_balance=True)
if len(p)>0:
raise ValidationError(
_("There are already payment method(s) for user balance")
)
# TODO : change Paiement to Payment
class Paiement(RevMixin, AclMixin, models.Model):
"""
......@@ -576,10 +632,22 @@ class Paiement(RevMixin, AclMixin, models.Model):
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")
......@@ -604,6 +672,62 @@ class Paiement(RevMixin, AclMixin, models.Model):
)
super(Paiement, self).save(*args, **kwargs)
def end_payment(self, invoice, request, use_payment_method=True):
"""
The general way of ending a payment.
:param invoice: The invoice being created.
:param request: Request sended by the user.
:param use_payment_method: If `self` has an attribute `payment_method`,
returns the result of
`self.payment_method.end_payment(invoice, request)`
:returns: An `HttpResponse`-like object.
"""
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()
}
)
# 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.
:param self: The payment
:param user: The user requesting usage
:returns: A boolean stating if usage is granted and an explanation
message if the boolean is `False`.
"""
return (
self.available_for_everyone
or user.has_perm('cotisations.use_every_payment'),
_("You cannot use this Payment.")
)
@classmethod
def find_allowed_payments(cls, user):
return [p for p in cls.objects.all() if p.can_use_payment(user)[0]]
class Cotisation(RevMixin, AclMixin, models.Model):
"""
......
# -*- 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,
]
# -*- 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
# -*- 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.
"""
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
)
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
# -*- 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
# -*- 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']
# -*- 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 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.
"""
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}
))
# -*- 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'
)
]
# -*- 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 .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 = getattr(invoice.paiement, 'payment_method', None)
if invoice.valid or not isinstance(payment_method, ChequePayment):
messages.error(
request,
_("You cannot pay this invoice with a cheque.")
)
return redirect(reverse(
'users:profil',
kwargs={'userid': request.user.pk}
))
form = 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()
}
)
# -*- 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
"""
......
# -*- 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