mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-25 22:22:26 +00:00
Conflicts fix switch_conf_json
# Conflicts: # re2o/templatetags/acl.py # re2o/views.py # topologie/templates/topologie/aff_port.html
This commit is contained in:
commit
feddc3f69b
128 changed files with 4328 additions and 2861 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,7 +1,7 @@
|
||||||
## MR 160: Datepicker
|
## MR 160: Datepicker
|
||||||
|
|
||||||
Install libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap javascript-common
|
Install libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap javascript-common
|
||||||
```
|
```bash
|
||||||
apt-get -y install \
|
apt-get -y install \
|
||||||
libjs-jquery \
|
libjs-jquery \
|
||||||
libjs-jquery-ui \
|
libjs-jquery-ui \
|
||||||
|
@ -10,12 +10,12 @@ apt-get -y install \
|
||||||
javascript-common
|
javascript-common
|
||||||
```
|
```
|
||||||
Enable javascript-common conf
|
Enable javascript-common conf
|
||||||
```
|
```bash
|
||||||
a2enconf javascript-common
|
a2enconf javascript-common
|
||||||
```
|
```
|
||||||
|
|
||||||
Delete old jquery files :
|
Delete old jquery files :
|
||||||
```
|
```bash
|
||||||
rm -r static_files/js/jquery-ui-*
|
rm -r static_files/js/jquery-ui-*
|
||||||
rm static_files/js/jquery-2.2.4.min.js
|
rm static_files/js/jquery-2.2.4.min.js
|
||||||
rm static/css/jquery-ui-timepicker-addon.css
|
rm static/css/jquery-ui-timepicker-addon.css
|
||||||
|
@ -42,6 +42,7 @@ Refactored install_re2o.sh script.
|
||||||
```
|
```
|
||||||
install_re2o.sh help
|
install_re2o.sh help
|
||||||
```
|
```
|
||||||
|
|
||||||
* The installation templates (LDIF files and `re2o/settings_locale.example.py`) have been changed to use `example.net` instead of `example.org` (more neutral and generic)
|
* The installation templates (LDIF files and `re2o/settings_locale.example.py`) have been changed to use `example.net` instead of `example.org` (more neutral and generic)
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,7 +76,6 @@ OPTIONAL_APPS = (
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## MR 177: Add django-debug-toolbar support
|
## MR 177: Add django-debug-toolbar support
|
||||||
|
|
||||||
Add the possibility to enable `django-debug-toolbar` in debug mode. First install the APT package:
|
Add the possibility to enable `django-debug-toolbar` in debug mode. First install the APT package:
|
||||||
|
@ -94,3 +94,29 @@ 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"]
|
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
|
||||||
|
After this modification you need to:
|
||||||
|
* Double-check your defined groups' unix-name only contain small letters
|
||||||
|
* Run the following commands to rebuild your ldap's groups:
|
||||||
|
```shell
|
||||||
|
python3 manage.py ldap_rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
* You may need to force your nslcd cache to be reloaded on some servers (else you will have to wait for the cache to be refreshed):
|
||||||
|
```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.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
@ -190,6 +191,13 @@ class MxSerializer(NamespacedHMSerializer):
|
||||||
fields = ('zone', 'priority', 'name', 'api_url')
|
fields = ('zone', 'priority', 'name', 'api_url')
|
||||||
|
|
||||||
|
|
||||||
|
class DNameSerializer(NamespacedHMSerializer):
|
||||||
|
"""Serialize `machines.models.DName` objects.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = machines.DName
|
||||||
|
fields = ('zone', 'alias', 'api_url')
|
||||||
|
|
||||||
class NsSerializer(NamespacedHMSerializer):
|
class NsSerializer(NamespacedHMSerializer):
|
||||||
"""Serialize `machines.models.Ns` objects.
|
"""Serialize `machines.models.Ns` objects.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# 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/mx', views.MxViewSet)
|
||||||
router.register_viewset(r'machines/ns', views.NsViewSet)
|
router.register_viewset(r'machines/ns', views.NsViewSet)
|
||||||
router.register_viewset(r'machines/txt', views.TxtViewSet)
|
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/srv', views.SrvViewSet)
|
||||||
router.register_viewset(r'machines/interface', views.InterfaceViewSet)
|
router.register_viewset(r'machines/interface', views.InterfaceViewSet)
|
||||||
router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet)
|
router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
@ -162,6 +163,12 @@ class TxtViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = machines.Txt.objects.all()
|
queryset = machines.Txt.objects.all()
|
||||||
serializer_class = serializers.TxtSerializer
|
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):
|
class SrvViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""Exposes list and details of `machines.models.Srv` objects.
|
"""Exposes list and details of `machines.models.Srv` objects.
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
# Copyright © 2017 Gabriel Détraz
|
# Copyright © 2017 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
|
# Copyright © 2018 Hugo Levy-Falk
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# 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.core.validators import MinValueValidator
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy as _l
|
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.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
from .models import Article, Paiement, Facture, Banque
|
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
|
Form used to manage and create an invoice and its fields.
|
||||||
cheque number.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
def __init__(self, *args, creation=False, **kwargs):
|
||||||
|
user = kwargs['user']
|
||||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||||
super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs)
|
super(FactureForm, 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")
|
|
||||||
self.fields['paiement'].empty_label = \
|
self.fields['paiement'].empty_label = \
|
||||||
_("Select a payment method")
|
_("Select a payment method")
|
||||||
paiement_list = Paiement.objects.filter(type_paiement=1)
|
self.fields['paiement'].queryset = Paiement.find_allowed_payments(user)
|
||||||
if paiement_list:
|
if not creation:
|
||||||
self.fields['paiement'].widget\
|
self.fields['user'].label = _("Member")
|
||||||
.attrs['data-cheque'] = paiement_list.first().id
|
self.fields['user'].empty_label = \
|
||||||
|
_("Select the proprietary member")
|
||||||
|
self.fields['valid'].label = _("Validated invoice")
|
||||||
|
else:
|
||||||
|
self.fields = {'paiement': self.fields['paiement']}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Facture
|
model = Facture
|
||||||
fields = ['paiement', 'banque', 'cheque']
|
fields = '__all__'
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(NewFactureForm, self).clean()
|
cleaned_data = super(FactureForm, self).clean()
|
||||||
paiement = cleaned_data.get('paiement')
|
paiement = cleaned_data.get('paiement')
|
||||||
cheque = cleaned_data.get('cheque')
|
|
||||||
banque = cleaned_data.get('banque')
|
|
||||||
if not paiement:
|
if not paiement:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("A payment method must be specified.")
|
_("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
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class CreditSoldeForm(NewFactureForm):
|
class SelectUserArticleForm(FormRevMixin, Form):
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Form used to select an article during the creation of an invoice for a
|
Form used to select an article during the creation of an invoice for a
|
||||||
member.
|
member.
|
||||||
|
@ -127,6 +102,11 @@ class SelectUserArticleForm(
|
||||||
required=True
|
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):
|
class SelectClubArticleForm(Form):
|
||||||
"""
|
"""
|
||||||
|
@ -146,6 +126,10 @@ class SelectClubArticleForm(Form):
|
||||||
required=True
|
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
|
# TODO : change Facture to Invoice
|
||||||
class NewFactureFormPdf(Form):
|
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):
|
class ArticleForm(FormRevMixin, ModelForm):
|
||||||
"""
|
"""
|
||||||
Form used to create an article.
|
Form used to create an article.
|
||||||
|
@ -231,17 +195,12 @@ class PaiementForm(FormRevMixin, ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Paiement
|
model = Paiement
|
||||||
# TODO : change moyen to method and type_paiement to payment_type
|
# 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):
|
def __init__(self, *args, **kwargs):
|
||||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||||
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
|
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||||
self.fields['moyen'].label = _("Payment method name")
|
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
|
# TODO : change paiement to payment
|
||||||
|
@ -304,56 +263,6 @@ class DelBanqueForm(FormRevMixin, Form):
|
||||||
self.fields['banques'].queryset = Banque.objects.all()
|
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
|
# TODO : Better name and docstring
|
||||||
class RechargeForm(FormRevMixin, Form):
|
class RechargeForm(FormRevMixin, Form):
|
||||||
"""
|
"""
|
||||||
|
@ -364,34 +273,31 @@ class RechargeForm(FormRevMixin, Form):
|
||||||
min_value=0.01,
|
min_value=0.01,
|
||||||
validators=[]
|
validators=[]
|
||||||
)
|
)
|
||||||
|
payment = forms.ModelChoiceField(
|
||||||
|
queryset=Paiement.objects.none(),
|
||||||
|
label=_l("Payment method")
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
self.user = kwargs.pop('user')
|
self.user = user
|
||||||
super(RechargeForm, self).__init__(*args, **kwargs)
|
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
|
the value is well inside the possible limits
|
||||||
"""
|
"""
|
||||||
value = self.cleaned_data['value']
|
value = self.cleaned_data['value']
|
||||||
if value < OptionalUser.get_cached_value('min_online_payment'):
|
balance_method = get_object_or_404(balance.PaymentMethod)
|
||||||
raise forms.ValidationError(
|
if balance_method.maximum_balance is not None and \
|
||||||
_("Requested amount is too small. Minimum amount possible : \
|
value + self.user.solde > balance_method.maximum_balance:
|
||||||
%(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'):
|
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("Requested amount is too high. Your balance can't exceed \
|
_("Requested amount is too high. Your balance can't exceed \
|
||||||
%(max_online_balance)s €.") % {
|
%(max_online_balance)s €.") % {
|
||||||
'max_online_balance': OptionalUser.get_cached_value(
|
'max_online_balance': balance_method.maximum_balance
|
||||||
'max_solde'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
|
# Copyright © 2018 Hugo Levy-Falk
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# 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 import timezone
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy as _l
|
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 machines.models import regen
|
||||||
from re2o.field_permissions import FieldPermissionModelMixin
|
from re2o.field_permissions import FieldPermissionModelMixin
|
||||||
from re2o.mixins import AclMixin, RevMixin
|
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
|
# TODO : change facture to invoice
|
||||||
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||||
|
@ -148,7 +155,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||||
models.F('prix')*models.F('number'),
|
models.F('prix')*models.F('number'),
|
||||||
output_field=models.FloatField()
|
output_field=models.FloatField()
|
||||||
)
|
)
|
||||||
)['total']
|
)['total'] or 0
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
"""
|
"""
|
||||||
|
@ -213,6 +220,22 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||||
_("You don't have the right to edit an invoice.")
|
_("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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Facture, self).__init__(*args, **kwargs)
|
super(Facture, self).__init__(*args, **kwargs)
|
||||||
self.field_permissions = {
|
self.field_permissions = {
|
||||||
|
@ -501,12 +524,17 @@ class Article(RevMixin, AclMixin, models.Model):
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_l("Type of cotisation")
|
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')
|
unique_together = ('name', 'type_user')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('view_article', _l("Can see an article's details")),
|
('view_article', _l("Can see an article's details")),
|
||||||
|
('buy_every_article', _l("Can buy every_article"))
|
||||||
)
|
)
|
||||||
verbose_name = "Article"
|
verbose_name = "Article"
|
||||||
verbose_name_plural = "Articles"
|
verbose_name_plural = "Articles"
|
||||||
|
@ -524,6 +552,35 @@ class Article(RevMixin, AclMixin, models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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):
|
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.
|
invoice. It's easier to know this information when doing the accouts.
|
||||||
It is represented by:
|
It is represented by:
|
||||||
* a name
|
* 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
|
# TODO : change moyen to method
|
||||||
moyen = models.CharField(
|
moyen = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_l("Method")
|
verbose_name=_l("Method")
|
||||||
)
|
)
|
||||||
type_paiement = models.IntegerField(
|
available_for_everyone = models.BooleanField(
|
||||||
choices=PAYMENT_TYPES,
|
default=False,
|
||||||
default=0,
|
verbose_name=_l("Is available for every user")
|
||||||
verbose_name=_l("Payment type")
|
)
|
||||||
|
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:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('view_paiement', _l("Can see a payement's details")),
|
('view_paiement', _l("Can see a payement's details")),
|
||||||
|
('use_every_payment', _l("Can use every payement")),
|
||||||
)
|
)
|
||||||
verbose_name = _l("Payment method")
|
verbose_name = _l("Payment method")
|
||||||
verbose_name_plural = _l("Payment methods")
|
verbose_name_plural = _l("Payment methods")
|
||||||
|
@ -593,16 +650,79 @@ class Paiement(RevMixin, AclMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
self.moyen = self.moyen.title()
|
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
|
The general way of ending a payment.
|
||||||
method of type 'cheque' exists.
|
|
||||||
|
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:
|
payment_method = find_payment_method(self)
|
||||||
raise ValidationError(
|
if payment_method is not None and use_payment_method:
|
||||||
_("You cannot have multiple payment method of type cheque")
|
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):
|
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
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
class Payment():
|
class Transaction():
|
||||||
""" The class representing a transaction with all the functions
|
""" The class representing a transaction with all the functions
|
||||||
used during the negociation
|
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
|
"""Payment
|
||||||
|
|
||||||
Here are defined some views dedicated to online payement.
|
Here are the views needed by comnpay
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
@ -14,22 +34,36 @@ from django.utils.datastructures import MultiValueDictKeyError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
|
|
||||||
from preferences.models import AssoOption
|
from cotisations.models import Facture
|
||||||
from .models import Facture
|
from .comnpay import Transaction
|
||||||
from .payment_utils.comnpay import Payment as ComnpayPayment
|
from .models import ComnpayPayment
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
def accept_payment(request, factureid):
|
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)
|
invoice = get_object_or_404(Facture, id=factureid)
|
||||||
|
if invoice.valid:
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("The payment of %(amount)s € has been accepted.") % {
|
_("The payment of %(amount)s € has been accepted.") % {
|
||||||
'amount': facture.prix()
|
'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(
|
return redirect(reverse(
|
||||||
|
@ -42,7 +76,8 @@ def accept_payment(request, factureid):
|
||||||
@login_required
|
@login_required
|
||||||
def refuse_payment(request):
|
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(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
|
@ -59,37 +94,38 @@ def ipn(request):
|
||||||
"""
|
"""
|
||||||
The view called by Comnpay server to validate the transaction.
|
The view called by Comnpay server to validate the transaction.
|
||||||
Verify that we can firmly save the user's action and notify
|
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', )
|
order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
|
||||||
try:
|
try:
|
||||||
data = OrderedDict([(f, request.POST[f]) for f in order])
|
data = OrderedDict([(f, request.POST[f]) for f in order])
|
||||||
except MultiValueDictKeyError:
|
except MultiValueDictKeyError:
|
||||||
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
|
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']
|
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:
|
try:
|
||||||
factureid = int(idTransaction)
|
factureid = int(idTransaction)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
|
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
|
||||||
|
|
||||||
facture = get_object_or_404(Facture, id=factureid)
|
facture = get_object_or_404(Facture, id=factureid)
|
||||||
|
payment_method = get_object_or_404(
|
||||||
|
ComnpayPayment, payment=facture.paiement)
|
||||||
|
|
||||||
|
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
|
# Checking that the payment is valid
|
||||||
if not result:
|
if not result:
|
||||||
# Payment failed: Cancelling the invoice operation
|
# Payment failed: Cancelling the invoice operation
|
||||||
facture.delete()
|
|
||||||
# And send the response to Comnpay indicating we have well
|
# And send the response to Comnpay indicating we have well
|
||||||
# received the failure information.
|
# received the failure information.
|
||||||
return HttpResponse("HTTP/1.1 200 OK")
|
return HttpResponse("HTTP/1.1 200 OK")
|
||||||
|
@ -100,42 +136,3 @@ def ipn(request):
|
||||||
# Everything worked we send a reponse to Comnpay indicating that
|
# Everything worked we send a reponse to Comnpay indicating that
|
||||||
# it's ok for them to proceed
|
# it's ok for them to proceed
|
||||||
return HttpResponse("HTTP/1.0 200 OK")
|
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')),
|
||||||
|
]
|
|
@ -24,6 +24,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -33,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<th>{% trans "Cotisation type" %}</th>
|
<th>{% trans "Cotisation type" %}</th>
|
||||||
<th>{% trans "Duration (month)" %}</th>
|
<th>{% trans "Duration (month)" %}</th>
|
||||||
<th>{% trans "Concerned users" %}</th>
|
<th>{% trans "Concerned users" %}</th>
|
||||||
|
<th>{% trans "Available for everyone" | tick %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -43,15 +46,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ article.type_cotisation }}</td>
|
<td>{{ article.type_cotisation }}</td>
|
||||||
<td>{{ article.duration }}</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">
|
<td class="text-right">
|
||||||
{% can_edit article %}
|
{% can_edit article %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}">
|
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'article' article.id %}">
|
{% history_button article %}
|
||||||
<i class="fa fa-history"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -41,9 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'banque' banque.id %}">
|
{% history_button banque %}
|
||||||
<i class="fa fa-history"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
{% if facture_list.paginator %}
|
{% if facture_list.paginator %}
|
||||||
|
@ -86,9 +87,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</li>
|
</li>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'cotisations:history' 'facture' facture.id %}">
|
{% history_button facture text=True html_class=False%}
|
||||||
<i class="fa fa-history"></i> {% trans "Historique" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,26 +24,32 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for paiement in paiement_list %}
|
{% for paiement in paiement_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ paiement.moyen }}</td>
|
<td>{{ paiement.moyen }}</td>
|
||||||
|
<td>{{ paiement.available_for_everyone|tick }}</td>
|
||||||
|
<td>
|
||||||
|
{{paiement.get_payment_method_name}}
|
||||||
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% can_edit paiement %}
|
{% can_edit paiement %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}">
|
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'paiement' paiement.id %}">
|
{% history_button paiement %}
|
||||||
<i class="fa fa-history"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -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 title %}{% trans "Invoices creation and edition" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form factureform %}
|
||||||
|
{% if payment_method %}
|
||||||
|
{% bootstrap_form payment_method %}
|
||||||
|
<div id="paymentMethod"></div>
|
||||||
|
{% endif %}
|
||||||
{% if articlesformset %}
|
{% if articlesformset %}
|
||||||
<h3>{% trans "Invoice's articles" %}</h3>
|
<h3>{% trans "Invoice's articles" %}</h3>
|
||||||
<div id="form_set" class="form-group">
|
<div id="form_set" class="form-group">
|
||||||
|
@ -56,14 +73,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_form factureform %}
|
|
||||||
{% bootstrap_button action_name button_type='submit' icon='star' %}
|
{% bootstrap_button action_name button_type='submit' icon='star' %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if articlesformset %}
|
{% if articlesformset or payment_method%}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
{% if articlesformset %}
|
||||||
var prices = {};
|
var prices = {};
|
||||||
{% for article in articles %}
|
{% for article in articlelist %}
|
||||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
@ -134,6 +151,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
}
|
}
|
||||||
update_price();
|
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>
|
</script>
|
||||||
{% endif %}
|
{% 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 %}
|
{% block content %}
|
||||||
<h3>
|
<h3>
|
||||||
{% blocktrans %}
|
{% blocktrans %}
|
||||||
Refill of {{ amount }} €
|
Pay {{ amount }} €
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</h3>
|
</h3>
|
||||||
<form class="form" method="{{ method }}" action="{{ action }}">
|
<form class="form" method="{{ method | default:"post" }}" action="{{ action }}">
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
|
{% if form %}
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% endif %}
|
||||||
{% trans "Pay" as tr_pay %}
|
{% trans "Pay" as tr_pay %}
|
||||||
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
|
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -27,9 +27,8 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
import re2o
|
|
||||||
from . import views
|
from . import views
|
||||||
from . import payment
|
from . import payment_methods
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(
|
url(
|
||||||
|
@ -122,41 +121,10 @@ urlpatterns = [
|
||||||
views.index_paiement,
|
views.index_paiement,
|
||||||
name='index-paiement'
|
name='index-paiement'
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r'history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
|
|
||||||
re2o.views.history,
|
|
||||||
name='history',
|
|
||||||
kwargs={'application': 'cotisations'},
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r'^control/$',
|
r'^control/$',
|
||||||
views.control,
|
views.control,
|
||||||
name='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'),
|
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 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
|
# Copyright © 2018 Hugo Levy-Falk
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -31,7 +32,7 @@ from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.urls import reverse
|
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.auth.decorators import login_required
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
|
@ -56,11 +57,10 @@ from re2o.acl import (
|
||||||
can_delete_set,
|
can_delete_set,
|
||||||
can_change,
|
can_change,
|
||||||
)
|
)
|
||||||
from preferences.models import OptionalUser, AssoOption, GeneralOption
|
from preferences.models import AssoOption, GeneralOption
|
||||||
from .models import Facture, Article, Vente, Paiement, Banque
|
from .models import Facture, Article, Vente, Paiement, Banque
|
||||||
from .forms import (
|
from .forms import (
|
||||||
NewFactureForm,
|
FactureForm,
|
||||||
EditFactureForm,
|
|
||||||
ArticleForm,
|
ArticleForm,
|
||||||
DelArticleForm,
|
DelArticleForm,
|
||||||
PaiementForm,
|
PaiementForm,
|
||||||
|
@ -70,11 +70,11 @@ from .forms import (
|
||||||
NewFactureFormPdf,
|
NewFactureFormPdf,
|
||||||
SelectUserArticleForm,
|
SelectUserArticleForm,
|
||||||
SelectClubArticleForm,
|
SelectClubArticleForm,
|
||||||
CreditSoldeForm,
|
|
||||||
RechargeForm
|
RechargeForm
|
||||||
)
|
)
|
||||||
from . import payment as online_payment
|
|
||||||
from .tex import render_invoice
|
from .tex import render_invoice
|
||||||
|
from .payment_methods.forms import payment_method_factory
|
||||||
|
from .utils import find_payment_method
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -84,29 +84,33 @@ def new_facture(request, user, userid):
|
||||||
"""
|
"""
|
||||||
View called to create a new invoice.
|
View called to create a new invoice.
|
||||||
Currently, Send the list of available articles for the user along with
|
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.
|
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
|
If everything is correct, save each one of the articles, save the
|
||||||
purchase object associated and finally the newly created invoice.
|
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)
|
invoice = Facture(user=user)
|
||||||
# The template needs the list of articles (for the JS part)
|
# The template needs the list of articles (for the JS part)
|
||||||
article_list = Article.objects.filter(
|
article_list = Article.objects.filter(
|
||||||
Q(type_user='All') | Q(type_user=request.user.class_name)
|
Q(type_user='All') | Q(type_user=request.user.class_name)
|
||||||
)
|
)
|
||||||
# Building the invocie form and the article formset
|
# Building the invoice form and the article formset
|
||||||
invoice_form = NewFactureForm(request.POST or None, instance=invoice)
|
invoice_form = FactureForm(
|
||||||
|
request.POST or None,
|
||||||
|
instance=invoice,
|
||||||
|
user=request.user,
|
||||||
|
creation=True
|
||||||
|
)
|
||||||
|
|
||||||
if request.user.is_class_club:
|
if request.user.is_class_club:
|
||||||
article_formset = formset_factory(SelectClubArticleForm)(
|
article_formset = formset_factory(SelectClubArticleForm)(
|
||||||
request.POST or None
|
request.POST or None,
|
||||||
|
form_kwargs={'user': request.user}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
article_formset = formset_factory(SelectUserArticleForm)(
|
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():
|
if invoice_form.is_valid() and article_formset.is_valid():
|
||||||
|
@ -114,42 +118,15 @@ def new_facture(request, user, userid):
|
||||||
articles = article_formset
|
articles = article_formset
|
||||||
# Check if at leat one article has been selected
|
# Check if at leat one article has been selected
|
||||||
if any(art.cleaned_data for art in articles):
|
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
|
# Building a purchase for each article sold
|
||||||
|
purchases = []
|
||||||
|
total_price = 0
|
||||||
for art_item in articles:
|
for art_item in articles:
|
||||||
if art_item.cleaned_data:
|
if art_item.cleaned_data:
|
||||||
article = art_item.cleaned_data['article']
|
article = art_item.cleaned_data['article']
|
||||||
quantity = art_item.cleaned_data['quantity']
|
quantity = art_item.cleaned_data['quantity']
|
||||||
new_purchase = Vente.objects.create(
|
total_price += article.prix*quantity
|
||||||
|
new_purchase = Vente(
|
||||||
facture=new_invoice_instance,
|
facture=new_invoice_instance,
|
||||||
name=article.name,
|
name=article.name,
|
||||||
prix=article.prix,
|
prix=article.prix,
|
||||||
|
@ -157,41 +134,42 @@ def new_facture(request, user, userid):
|
||||||
duration=article.duration,
|
duration=article.duration,
|
||||||
number=quantity
|
number=quantity
|
||||||
)
|
)
|
||||||
new_purchase.save()
|
purchases.append(new_purchase)
|
||||||
|
p = find_payment_method(new_invoice_instance.paiement)
|
||||||
# In case a cotisation was bought, inform the user, the
|
if hasattr(p, 'check_price'):
|
||||||
# cotisation time has been extended too
|
price_ok, msg = p.check_price(total_price, user)
|
||||||
if any(art_item.cleaned_data['article'].type_cotisation
|
invoice_form.add_error(None, msg)
|
||||||
for art_item in articles if art_item.cleaned_data):
|
|
||||||
messages.success(
|
|
||||||
request,
|
|
||||||
_("The cotisation of %(member_name)s has been \
|
|
||||||
extended to %(end_date)s.") % {
|
|
||||||
'member_name': user.pseudo,
|
|
||||||
'end_date': user.end_adhesion()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Else, only tell the invoice was created
|
|
||||||
else:
|
else:
|
||||||
messages.success(
|
price_ok = True
|
||||||
request,
|
if price_ok:
|
||||||
_("The invoice has been created.")
|
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(
|
else:
|
||||||
'users:profil',
|
|
||||||
kwargs={'userid': userid}
|
|
||||||
))
|
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
_("You need to choose at least one article.")
|
_("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(
|
return form(
|
||||||
{
|
{
|
||||||
'factureform': invoice_form,
|
'factureform': invoice_form,
|
||||||
'venteform': article_formset,
|
'articlesformset': article_formset,
|
||||||
'articlelist': article_list
|
'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)
|
invoice_form = NewFactureFormPdf(request.POST or None)
|
||||||
if request.user.is_class_club:
|
if request.user.is_class_club:
|
||||||
articles_formset = formset_factory(SelectClubArticleForm)(
|
articles_formset = formset_factory(SelectClubArticleForm)(
|
||||||
request.POST or None
|
request.POST or None,
|
||||||
|
form_kwargs={'user': request.user}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
articles_formset = formset_factory(SelectUserArticleForm)(
|
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():
|
if invoice_form.is_valid() and articles_formset.is_valid():
|
||||||
# Get the article list and build an list out of it
|
# Get the article list and build an list out of it
|
||||||
|
@ -313,7 +293,7 @@ def edit_facture(request, facture, **_kwargs):
|
||||||
can be set as desired. This is also the view used to invalidate
|
can be set as desired. This is also the view used to invalidate
|
||||||
an invoice.
|
an invoice.
|
||||||
"""
|
"""
|
||||||
invoice_form = EditFactureForm(
|
invoice_form = FactureForm(
|
||||||
request.POST or None,
|
request.POST or None,
|
||||||
instance=facture,
|
instance=facture,
|
||||||
user=request.user
|
user=request.user
|
||||||
|
@ -364,39 +344,6 @@ def del_facture(request, facture, **_kwargs):
|
||||||
}, 'cotisations/delete.html', request)
|
}, 'cotisations/delete.html', request)
|
||||||
|
|
||||||
|
|
||||||
# TODO : change solde to balance
|
|
||||||
@login_required
|
|
||||||
@can_create(Facture)
|
|
||||||
@can_edit(User)
|
|
||||||
def credit_solde(request, user, **_kwargs):
|
|
||||||
"""
|
|
||||||
View used to edit the balance of a user.
|
|
||||||
Can be use either to increase or decrease a user's balance.
|
|
||||||
"""
|
|
||||||
# TODO : change facture to invoice
|
|
||||||
invoice = CreditSoldeForm(request.POST or None)
|
|
||||||
if invoice.is_valid():
|
|
||||||
invoice_instance = invoice.save(commit=False)
|
|
||||||
invoice_instance.user = user
|
|
||||||
invoice_instance.save()
|
|
||||||
new_purchase = Vente.objects.create(
|
|
||||||
facture=invoice_instance,
|
|
||||||
name="solde",
|
|
||||||
prix=invoice.cleaned_data['montant'],
|
|
||||||
number=1
|
|
||||||
)
|
|
||||||
new_purchase.save()
|
|
||||||
messages.success(
|
|
||||||
request,
|
|
||||||
_("Balance successfully updated.")
|
|
||||||
)
|
|
||||||
return redirect(reverse('cotisations:index'))
|
|
||||||
return form({
|
|
||||||
'factureform': invoice,
|
|
||||||
'action_name': _("Edit")
|
|
||||||
}, 'cotisations/facture.html', request)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_create(Article)
|
@can_create(Article)
|
||||||
def add_article(request):
|
def add_article(request):
|
||||||
|
@ -419,7 +366,8 @@ def add_article(request):
|
||||||
return redirect(reverse('cotisations:index-article'))
|
return redirect(reverse('cotisations:index-article'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': article,
|
'factureform': article,
|
||||||
'action_name': _("Add")
|
'action_name': _("Add"),
|
||||||
|
'title': _("New article")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -440,7 +388,8 @@ def edit_article(request, article_instance, **_kwargs):
|
||||||
return redirect(reverse('cotisations:index-article'))
|
return redirect(reverse('cotisations:index-article'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': article,
|
'factureform': article,
|
||||||
'action_name': _('Edit')
|
'action_name': _('Edit'),
|
||||||
|
'title': _("Edit article")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -461,7 +410,8 @@ def del_article(request, instances):
|
||||||
return redirect(reverse('cotisations:index-article'))
|
return redirect(reverse('cotisations:index-article'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': article,
|
'factureform': article,
|
||||||
'action_name': _("Delete")
|
'action_name': _("Delete"),
|
||||||
|
'title': _("Delete article")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -472,9 +422,15 @@ def add_paiement(request):
|
||||||
"""
|
"""
|
||||||
View used to add a payment method.
|
View used to add a payment method.
|
||||||
"""
|
"""
|
||||||
payment = PaiementForm(request.POST or None)
|
payment = PaiementForm(request.POST or None, prefix='payment')
|
||||||
if payment.is_valid():
|
payment_method = payment_method_factory(
|
||||||
payment.save()
|
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(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("The payment method has been successfully created.")
|
_("The payment method has been successfully created.")
|
||||||
|
@ -482,7 +438,9 @@ def add_paiement(request):
|
||||||
return redirect(reverse('cotisations:index-paiement'))
|
return redirect(reverse('cotisations:index-paiement'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': payment,
|
'factureform': payment,
|
||||||
'action_name': _("Add")
|
'payment_method': payment_method,
|
||||||
|
'action_name': _("Add"),
|
||||||
|
'title': _("New payment method")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -493,10 +451,23 @@ def edit_paiement(request, paiement_instance, **_kwargs):
|
||||||
"""
|
"""
|
||||||
View used to edit a payment method.
|
View used to edit a payment method.
|
||||||
"""
|
"""
|
||||||
payment = PaiementForm(request.POST or None, instance=paiement_instance)
|
payment = PaiementForm(
|
||||||
if payment.is_valid():
|
request.POST or None,
|
||||||
if payment.changed_data:
|
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()
|
payment.save()
|
||||||
|
if payment_method is not None:
|
||||||
|
payment_method.save()
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("The payement method has been successfully edited.")
|
_("The payement method has been successfully edited.")
|
||||||
|
@ -504,7 +475,9 @@ def edit_paiement(request, paiement_instance, **_kwargs):
|
||||||
return redirect(reverse('cotisations:index-paiement'))
|
return redirect(reverse('cotisations:index-paiement'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': payment,
|
'factureform': payment,
|
||||||
'action_name': _("Edit")
|
'payment_method': payment_method,
|
||||||
|
'action_name': _("Edit"),
|
||||||
|
'title': _("Edit payment method")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -539,7 +512,8 @@ def del_paiement(request, instances):
|
||||||
return redirect(reverse('cotisations:index-paiement'))
|
return redirect(reverse('cotisations:index-paiement'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': payment,
|
'factureform': payment,
|
||||||
'action_name': _("Delete")
|
'action_name': _("Delete"),
|
||||||
|
'title': _("Delete payment method")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -560,7 +534,8 @@ def add_banque(request):
|
||||||
return redirect(reverse('cotisations:index-banque'))
|
return redirect(reverse('cotisations:index-banque'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': bank,
|
'factureform': bank,
|
||||||
'action_name': _("Add")
|
'action_name': _("Add"),
|
||||||
|
'title': _("New bank")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -582,7 +557,8 @@ def edit_banque(request, banque_instance, **_kwargs):
|
||||||
return redirect(reverse('cotisations:index-banque'))
|
return redirect(reverse('cotisations:index-banque'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': bank,
|
'factureform': bank,
|
||||||
'action_name': _("Edit")
|
'action_name': _("Edit"),
|
||||||
|
'title': _("Edit bank")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -617,7 +593,8 @@ def del_banque(request, instances):
|
||||||
return redirect(reverse('cotisations:index-banque'))
|
return redirect(reverse('cotisations:index-banque'))
|
||||||
return form({
|
return form({
|
||||||
'factureform': bank,
|
'factureform': bank,
|
||||||
'action_name': _("Delete")
|
'action_name': _("Delete"),
|
||||||
|
'title': _("Delete bank")
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -722,153 +699,59 @@ def index(request):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# TODO : merge this function with new_facture() which is nearly the same
|
# TODO : change solde to balance
|
||||||
# TODO : change facture to invoice
|
|
||||||
@login_required
|
@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.
|
View used to edit the balance of a user.
|
||||||
Currently, send the list of available articles for the user along with
|
Can be use either to increase or decrease a user's balance.
|
||||||
a formset of a new invoice (based on the `:forms:NewFactureForm()` form.
|
|
||||||
A bit of JS is used in the template to add articles in a fancier way.
|
|
||||||
If everything is correct, save each one of the articles, save the
|
|
||||||
purchase object associated and finally the newly created invoice.
|
|
||||||
|
|
||||||
TODO : The whole verification process should be moved to the model. This
|
|
||||||
function should only act as a dumb interface between the model and the
|
|
||||||
user.
|
|
||||||
"""
|
"""
|
||||||
user = request.user
|
try:
|
||||||
invoice = Facture(user=user)
|
balance = find_payment_method(Paiement.objects.get(is_balance=True))
|
||||||
payment, _created = Paiement.objects.get_or_create(moyen='Solde')
|
except Paiement.DoesNotExist:
|
||||||
invoice.paiement = payment
|
credit_allowed = False
|
||||||
# The template needs the list of articles (for the JS part)
|
|
||||||
article_list = Article.objects.filter(
|
|
||||||
Q(type_user='All') | Q(type_user=request.user.class_name)
|
|
||||||
)
|
|
||||||
if request.user.is_class_club:
|
|
||||||
article_formset = formset_factory(SelectClubArticleForm)(
|
|
||||||
request.POST or None
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
article_formset = formset_factory(SelectUserArticleForm)(
|
credit_allowed = (
|
||||||
request.POST or None
|
balance is not None
|
||||||
|
and balance.can_credit_balance(request.user)
|
||||||
)
|
)
|
||||||
|
if not credit_allowed:
|
||||||
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(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
_("The balance is too low for this operation.")
|
_("You are not allowed to credit your balance.")
|
||||||
)
|
)
|
||||||
return redirect(reverse(
|
return redirect(reverse(
|
||||||
'users:profil',
|
'users:profil',
|
||||||
kwargs={'userid': userid}
|
kwargs={'userid': user.id}
|
||||||
))
|
|
||||||
# Saving the invoice
|
|
||||||
invoice.save()
|
|
||||||
|
|
||||||
# Building a purchase for each article sold
|
|
||||||
for art_item in articles:
|
|
||||||
if art_item.cleaned_data:
|
|
||||||
article = art_item.cleaned_data['article']
|
|
||||||
quantity = art_item.cleaned_data['quantity']
|
|
||||||
new_purchase = Vente.objects.create(
|
|
||||||
facture=invoice,
|
|
||||||
name=article.name,
|
|
||||||
prix=article.prix,
|
|
||||||
type_cotisation=article.type_cotisation,
|
|
||||||
duration=article.duration,
|
|
||||||
number=quantity
|
|
||||||
)
|
|
||||||
new_purchase.save()
|
|
||||||
|
|
||||||
# In case a cotisation was bought, inform the user, the
|
|
||||||
# cotisation time has been extended too
|
|
||||||
if any(art_item.cleaned_data['article'].type_cotisation
|
|
||||||
for art_item in articles if art_item.cleaned_data):
|
|
||||||
messages.success(
|
|
||||||
request,
|
|
||||||
_("The cotisation of %(member_name)s has been successfully \
|
|
||||||
extended to %(end_date)s.") % {
|
|
||||||
'member_name': user.pseudo,
|
|
||||||
'end_date': user.end_adhesion()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Else, only tell the invoice was created
|
|
||||||
else:
|
|
||||||
messages.success(
|
|
||||||
request,
|
|
||||||
_("The invoice has been successuflly created.")
|
|
||||||
)
|
|
||||||
return redirect(reverse(
|
|
||||||
'users:profil',
|
|
||||||
kwargs={'userid': userid}
|
|
||||||
))
|
|
||||||
messages.error(
|
|
||||||
request,
|
|
||||||
_("You need to choose at least one article.")
|
|
||||||
)
|
|
||||||
return redirect(reverse(
|
|
||||||
'users:profil',
|
|
||||||
kwargs={'userid': userid}
|
|
||||||
))
|
))
|
||||||
|
|
||||||
return form({
|
|
||||||
'venteform': article_formset,
|
|
||||||
'articlelist': article_list
|
|
||||||
}, 'cotisations/new_facture_solde.html', request)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO : change 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)
|
refill_form = RechargeForm(request.POST or None, user=request.user)
|
||||||
if refill_form.is_valid():
|
if refill_form.is_valid():
|
||||||
invoice = Facture(user=request.user)
|
price = refill_form.cleaned_data['value']
|
||||||
payment, _created = Paiement.objects.get_or_create(
|
invoice = Facture(user=user)
|
||||||
moyen='Rechargement en ligne'
|
invoice.paiement = refill_form.cleaned_data['payment']
|
||||||
)
|
p = find_payment_method(invoice.paiement)
|
||||||
invoice.paiement = payment
|
if hasattr(p, 'check_price'):
|
||||||
invoice.valid = False
|
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()
|
invoice.save()
|
||||||
purchase = Vente.objects.create(
|
Vente.objects.create(
|
||||||
facture=invoice,
|
facture=invoice,
|
||||||
name='solde',
|
name='solde',
|
||||||
prix=refill_form.cleaned_data['value'],
|
prix=refill_form.cleaned_data['value'],
|
||||||
number=1
|
number=1
|
||||||
)
|
)
|
||||||
purchase.save()
|
return invoice.paiement.end_payment(invoice, request)
|
||||||
content = online_payment.PAYMENT_SYSTEM[
|
p = get_object_or_404(Paiement, is_balance=True)
|
||||||
AssoOption.get_cached_value('payment')
|
|
||||||
](invoice, request)
|
|
||||||
return render(request, 'cotisations/payment.html', content)
|
|
||||||
return form({
|
return form({
|
||||||
'rechargeform': refill_form
|
'factureform': refill_form,
|
||||||
}, 'cotisations/recharge.html', request)
|
'balance': request.user.solde,
|
||||||
|
'title': _("Refill your balance"),
|
||||||
|
'action_name': _("Pay"),
|
||||||
|
'max_balance': p.payment_method.maximum_balance,
|
||||||
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
{% 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 acl %}
|
|
||||||
|
|
||||||
{% for droit,users in stats_list.items %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse{{droit.id}}">
|
|
||||||
<h2 class="panel-title pull-left">
|
|
||||||
<i class="fa fa-address-book"></i>
|
|
||||||
{{droit}}
|
|
||||||
<span class="badge">{{users.count}}</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="panel-collapse collapse" id="collapse{{droit.id}}">
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Pseudo</th>
|
|
||||||
<th>Adhésion</th>
|
|
||||||
<th>Derniere connexion</th>
|
|
||||||
<th>Nombre d'actions</th>
|
|
||||||
<th>Date de la dernière action</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{% for utilisateur in users %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ utilisateur.pseudo }}</td>
|
|
||||||
{% if utilisateur.is_adherent %}
|
|
||||||
<td><p class="text-success">Adhérent</p></td>
|
|
||||||
{% elif not utilisateur.end_adhesion %}
|
|
||||||
<td><p class="text-warning">On ne s'en souvient plus...</p></td>
|
|
||||||
{% else %}
|
|
||||||
<td><p class="text-danger">Plus depuis {{ utilisateur.end_adhesion }}</p></td>
|
|
||||||
{% endif %}
|
|
||||||
<td>{{ utilisateur.last_login }}</td>
|
|
||||||
<td>{{ utilisateur.num }}</td>
|
|
||||||
{% if not utilisateur.last %}
|
|
||||||
<td><p class="text-danger">Jamais</p></td>
|
|
||||||
{% else %}
|
|
||||||
<td><p class="text-success">{{utilisateur.last}}</p></td>
|
|
||||||
{% endif %}
|
|
||||||
<td>
|
|
||||||
{% if droit != 'Superuser' %}
|
|
||||||
<a href="{% url 'users:del-group' utilisateur.id droit.id %}">
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'users:del-superuser' utilisateur.id %}">
|
|
||||||
{% endif %}
|
|
||||||
<button type="button" class="btn btn-danger" aria-label="Left Align">
|
|
||||||
<span class="fa fa-user-times" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
|
@ -51,9 +51,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<i class="fa fa-users"></i>
|
<i class="fa fa-users"></i>
|
||||||
Utilisateurs
|
Utilisateurs
|
||||||
</a>
|
</a>
|
||||||
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-droits" %}">
|
|
||||||
<i class="fa fa-balance-scale"></i>
|
|
||||||
Groupes de droit
|
|
||||||
</a>
|
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
{% extends "logs/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 %}
|
|
||||||
|
|
||||||
{% block title %}Statistiques des droits{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2>Statistiques des droits</h2>
|
|
||||||
{% include "logs/aff_stats_droits.html" with stats_list=stats_list %}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{% endblock %}
|
|
|
@ -33,3 +33,23 @@ register = template.Library()
|
||||||
def classname(obj):
|
def classname(obj):
|
||||||
""" Returns the object class name """
|
""" Returns the object class name """
|
||||||
return obj.__class__.__name__
|
return obj.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('buttons/history.html')
|
||||||
|
def history_button(instance, text=False, html_class=True):
|
||||||
|
"""Creates the correct history button for an instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: The instance of which you want to get history buttons.
|
||||||
|
text: Flag stating if a 'History' text should be displayed.
|
||||||
|
html_class: Flag stating if the link should have the html classes
|
||||||
|
allowing it to be displayed as a button.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'application': instance._meta.app_label,
|
||||||
|
'name': instance._meta.model_name,
|
||||||
|
'id': instance.id,
|
||||||
|
'text': text,
|
||||||
|
'class': html_class,
|
||||||
|
}
|
||||||
|
|
|
@ -39,5 +39,9 @@ urlpatterns = [
|
||||||
url(r'^stats_models/$', views.stats_models, name='stats-models'),
|
url(r'^stats_models/$', views.stats_models, name='stats-models'),
|
||||||
url(r'^stats_users/$', views.stats_users, name='stats-users'),
|
url(r'^stats_users/$', views.stats_users, name='stats-users'),
|
||||||
url(r'^stats_actions/$', views.stats_actions, name='stats-actions'),
|
url(r'^stats_actions/$', views.stats_actions, name='stats-actions'),
|
||||||
url(r'^stats_droits/$', views.stats_droits, name='stats-droits'),
|
url(
|
||||||
|
r'(?P<application>\w+)/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
|
||||||
|
views.history,
|
||||||
|
name='history',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
#
|
#
|
||||||
# Copyright © 2017 Gabriel Détraz
|
# Copyright © 2018 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2018 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2018 Augustin Lemesle
|
||||||
|
# Copyright © 2018 Hugo Levy-Falk
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -36,12 +37,16 @@ nombre d'objets par models, nombre d'actions par user, etc
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Count, Max, F
|
from django.http import Http404
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.apps import apps
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from reversion.models import Revision
|
from reversion.models import Revision
|
||||||
from reversion.models import Version, ContentType
|
from reversion.models import Version, ContentType
|
||||||
|
@ -455,25 +460,56 @@ def stats_actions(request):
|
||||||
return render(request, 'logs/stats_users.html', {'stats_list': stats})
|
return render(request, 'logs/stats_users.html', {'stats_list': stats})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
def history(request, application, object_name, object_id):
|
||||||
@can_view_app('users')
|
"""Render history for a model.
|
||||||
def stats_droits(request):
|
|
||||||
"""Affiche la liste des droits et les users ayant chaque droit"""
|
|
||||||
stats_list = {}
|
|
||||||
|
|
||||||
for droit in ListRight.objects.all().select_related('group_ptr'):
|
The model is determined using the `HISTORY_BIND` dictionnary if none is
|
||||||
stats_list[droit] = droit.user_set.all().annotate(
|
found, raises a Http404. The view checks if the user is allowed to see the
|
||||||
num=Count('revision'),
|
history using the `can_view` method of the model.
|
||||||
last=Max('revision__date_created'),
|
|
||||||
)
|
|
||||||
|
|
||||||
stats_list['Superuser'] = User.objects.filter(is_superuser=True).annotate(
|
Args:
|
||||||
num=Count('revision'),
|
request: The request sent by the user.
|
||||||
last=Max('revision__date_created'),
|
application: Name of the application.
|
||||||
)
|
object_name: Name of the model.
|
||||||
|
object_id: Id of the object you want to acces history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The rendered page of history if access is granted, else the user is
|
||||||
|
redirected to their profile page, with an error message.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Http404: This kind of models doesn't have history.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
model = apps.get_model(application, object_name)
|
||||||
|
except LookupError:
|
||||||
|
raise Http404(_("No model found."))
|
||||||
|
object_name_id = object_name + 'id'
|
||||||
|
kwargs = {object_name_id: object_id}
|
||||||
|
try:
|
||||||
|
instance = model.get_instance(**kwargs)
|
||||||
|
except model.DoesNotExist:
|
||||||
|
messages.error(request, _("No entry found."))
|
||||||
|
return redirect(reverse(
|
||||||
|
'users:profil',
|
||||||
|
kwargs={'userid': str(request.user.id)}
|
||||||
|
))
|
||||||
|
can, msg = instance.can_view(request.user)
|
||||||
|
if not can:
|
||||||
|
messages.error(request, msg or _("You cannot acces to this menu"))
|
||||||
|
return redirect(reverse(
|
||||||
|
'users:profil',
|
||||||
|
kwargs={'userid': str(request.user.id)}
|
||||||
|
))
|
||||||
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
|
reversions = Version.objects.get_for_object(instance)
|
||||||
|
if hasattr(instance, 'linked_objects'):
|
||||||
|
for related_object in chain(instance.linked_objects()):
|
||||||
|
reversions = (reversions |
|
||||||
|
Version.objects.get_for_object(related_object))
|
||||||
|
reversions = re2o_paginator(request, reversions, pagination_number)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'logs/stats_droits.html',
|
're2o/history.html',
|
||||||
{'stats_list': stats_list}
|
{'reversions': reversions, 'object': instance}
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ from .models import (
|
||||||
Ns,
|
Ns,
|
||||||
Vlan,
|
Vlan,
|
||||||
Txt,
|
Txt,
|
||||||
|
DName,
|
||||||
Srv,
|
Srv,
|
||||||
Nas,
|
Nas,
|
||||||
Service,
|
Service,
|
||||||
|
@ -95,6 +96,10 @@ class TxtAdmin(VersionAdmin):
|
||||||
""" Admin view of a TXT object """
|
""" Admin view of a TXT object """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class DNameAdmin(VersionAdmin):
|
||||||
|
""" Admin view of a DName object """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SrvAdmin(VersionAdmin):
|
class SrvAdmin(VersionAdmin):
|
||||||
""" Admin view of a SRV object """
|
""" Admin view of a SRV object """
|
||||||
|
@ -144,6 +149,7 @@ admin.site.register(SOA, SOAAdmin)
|
||||||
admin.site.register(Mx, MxAdmin)
|
admin.site.register(Mx, MxAdmin)
|
||||||
admin.site.register(Ns, NsAdmin)
|
admin.site.register(Ns, NsAdmin)
|
||||||
admin.site.register(Txt, TxtAdmin)
|
admin.site.register(Txt, TxtAdmin)
|
||||||
|
admin.site.register(DName, DNameAdmin)
|
||||||
admin.site.register(Srv, SrvAdmin)
|
admin.site.register(Srv, SrvAdmin)
|
||||||
admin.site.register(IpList, IpListAdmin)
|
admin.site.register(IpList, IpListAdmin)
|
||||||
admin.site.register(Interface, InterfaceAdmin)
|
admin.site.register(Interface, InterfaceAdmin)
|
||||||
|
|
|
@ -51,6 +51,7 @@ from .models import (
|
||||||
SOA,
|
SOA,
|
||||||
Mx,
|
Mx,
|
||||||
Txt,
|
Txt,
|
||||||
|
DName,
|
||||||
Ns,
|
Ns,
|
||||||
Service,
|
Service,
|
||||||
Vlan,
|
Vlan,
|
||||||
|
@ -410,6 +411,34 @@ class DelTxtForm(FormRevMixin, Form):
|
||||||
self.fields['txt'].queryset = Txt.objects.all()
|
self.fields['txt'].queryset = Txt.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class DNameForm(FormRevMixin, ModelForm):
|
||||||
|
"""Add a DNAME entry for a zone"""
|
||||||
|
class Meta:
|
||||||
|
model = DName
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||||
|
super(DNameForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DelDNameForm(FormRevMixin, Form):
|
||||||
|
"""Delete a set of DNAME entries"""
|
||||||
|
dnames = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Txt.objects.none(),
|
||||||
|
label="Existing DNAME entries",
|
||||||
|
widget=forms.CheckboxSelectMultiple
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
instances = kwargs.pop('instances', None)
|
||||||
|
super(DelDNameForm, self).__init__(*args, **kwargs)
|
||||||
|
if instances:
|
||||||
|
self.fields['dnames'].queryset = instances
|
||||||
|
else:
|
||||||
|
self.fields['dnames'].queryset = DName.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class SrvForm(FormRevMixin, ModelForm):
|
class SrvForm(FormRevMixin, ModelForm):
|
||||||
"""Ajout d'un srv pour une zone"""
|
"""Ajout d'un srv pour une zone"""
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
47
machines/migrations/0083_remove_duplicate_rights.py
Normal file
47
machines/migrations/0083_remove_duplicate_rights.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def remove_permission_alias(apps, schema_editor):
|
||||||
|
Permission = apps.get_model('auth', 'Permission')
|
||||||
|
for codename in ['add_alias', 'change_alias', 'delete_alias']:
|
||||||
|
# Retrieve the wrong permission
|
||||||
|
try:
|
||||||
|
to_remove = Permission.objects.get(
|
||||||
|
codename=codename,
|
||||||
|
content_type__model='domain'
|
||||||
|
)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
# The permission is missing so no problem
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
to_remove.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_permission_text(apps, schema_editor):
|
||||||
|
Permission = apps.get_model('auth', 'Permission')
|
||||||
|
for codename in ['add_text', 'change_text', 'delete_text']:
|
||||||
|
# Retrieve the wrong permission
|
||||||
|
try:
|
||||||
|
to_remove = Permission.objects.get(
|
||||||
|
codename=codename,
|
||||||
|
content_type__model='txt'
|
||||||
|
)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
# The permission is missing so no problem
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
to_remove.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0082_auto_20180525_2209'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(remove_permission_text),
|
||||||
|
migrations.RunPython(remove_permission_alias),
|
||||||
|
]
|
31
machines/migrations/0088_dname.py
Normal file
31
machines/migrations/0088_dname.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-06-25 14:33
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import re2o.mixins
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('machines', '0083_remove_duplicate_rights'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DName',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('alias', models.CharField(max_length=255)),
|
||||||
|
('zone', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='machines.Extension')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'permissions': (('view_dname', 'Can see a dname object'),),
|
||||||
|
'verbose_name': 'DNAME entry',
|
||||||
|
'verbose_name_plural': 'DNAME entries'
|
||||||
|
},
|
||||||
|
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
|
@ -3,9 +3,10 @@
|
||||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
#
|
#
|
||||||
# Copyright © 2017 Gabriel Détraz
|
# Copyright © 2016-2018 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
|
# Copyright © 2018 Charlie Jacomme
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -670,6 +671,27 @@ class Txt(RevMixin, AclMixin, models.Model):
|
||||||
return str(self.field1).ljust(15) + " IN TXT " + str(self.field2)
|
return str(self.field1).ljust(15) + " IN TXT " + str(self.field2)
|
||||||
|
|
||||||
|
|
||||||
|
class DName(RevMixin, AclMixin, models.Model):
|
||||||
|
"""A DNAME entry for the DNS."""
|
||||||
|
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
||||||
|
alias = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
("view_dname", "Can see a dname object"),
|
||||||
|
)
|
||||||
|
verbose_name = "DNAME entry"
|
||||||
|
verbose_name_plural = "DNAME entries"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.zone) + " : " + str(self.alias)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def dns_entry(self):
|
||||||
|
"""Returns the DNAME record for the DNS zone file."""
|
||||||
|
return str(self.alias).ljust(15) + " IN DNAME " + str(self.zone)
|
||||||
|
|
||||||
|
|
||||||
class Srv(RevMixin, AclMixin, models.Model):
|
class Srv(RevMixin, AclMixin, models.Model):
|
||||||
""" A SRV record """
|
""" A SRV record """
|
||||||
PRETTY_NAME = "Enregistrement Srv"
|
PRETTY_NAME = "Enregistrement Srv"
|
||||||
|
@ -1687,6 +1709,18 @@ def text_post_delete(**_kwargs):
|
||||||
regen('dns')
|
regen('dns')
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=DName)
|
||||||
|
def dname_post_save(**_kwargs):
|
||||||
|
"""Updates the DNS regen after modification of a DName object."""
|
||||||
|
regen('dns')
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=DName)
|
||||||
|
def dname_post_delete(**_kwargs):
|
||||||
|
"""Updates the DNS regen after deletion of a DName object."""
|
||||||
|
regen('dns')
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Srv)
|
@receiver(post_save, sender=Srv)
|
||||||
def srv_post_save(**_kwargs):
|
def srv_post_save(**_kwargs):
|
||||||
"""Regeneration dns après modification d'un SRV"""
|
"""Regeneration dns après modification d'un SRV"""
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -38,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit alias %}
|
{% can_edit alias %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='domain' id=alias.id %}
|
{% history_button alias %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
{% extends "cotisations/sidebar.html" %}
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
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
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
quelques clics.
|
quelques clics.
|
||||||
|
|
||||||
Copyright © 2017 Gabriel Détraz
|
Copyright © 2018 Charlie Jacomme
|
||||||
Copyright © 2017 Goulven Kermarec
|
|
||||||
Copyright © 2017 Augustin Lemesle
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,23 +20,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load acl %}
|
||||||
{% load staticfiles%}
|
|
||||||
{% load i18n %}
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Target zone</th>
|
||||||
|
<th>Record</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for dname in dname_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ dname.zone }}</td>
|
||||||
|
<td>{{ dname.dns_entry }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% can_edit dname %}
|
||||||
|
{% include 'buttons/edit.html' with href='machines:edit-dname' id=dname.id %}
|
||||||
|
{% acl_end %}
|
||||||
|
{% include 'buttons/history.html' with href='machines:history' name='dname' id=dname.id %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
{% 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 %}
|
|
|
@ -23,6 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
|
@ -41,7 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% for extension in extension_list %}
|
{% for extension in extension_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ extension.name }}</td>
|
<td>{{ extension.name }}</td>
|
||||||
<td>{{ extension.need_infra }}</td>
|
<td>{{ extension.need_infra|tick }}</td>
|
||||||
<td>{{ extension.soa}}</td>
|
<td>{{ extension.soa}}</td>
|
||||||
<td>{{ extension.origin }}</td>
|
<td>{{ extension.origin }}</td>
|
||||||
{% if ipv6_enabled %}
|
{% if ipv6_enabled %}
|
||||||
|
@ -51,7 +53,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit extension %}
|
{% can_edit extension %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='extension' id=extension.id %}
|
{% history_button extension %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -22,7 +22,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -42,7 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ type.type }}</td>
|
<td>{{ type.type }}</td>
|
||||||
<td>{{ type.extension }}</td>
|
<td>{{ type.extension }}</td>
|
||||||
<td>{{ type.need_infra }}</td>
|
<td>{{ type.need_infra|tick }}</td>
|
||||||
<td>{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}</td>
|
<td>{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}</td>
|
||||||
<td>{{ type.prefix_v6 }}</td>
|
<td>{{ type.prefix_v6 }}</td>
|
||||||
<td>{{ type.vlan }}</td>
|
<td>{{ type.vlan }}</td>
|
||||||
|
@ -51,7 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit type %}
|
{% can_edit type %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='iptype' id=type.id %}
|
{% history_button type %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_delete ipv6 %}
|
{% can_delete ipv6 %}
|
||||||
{% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %}
|
{% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='ipv6list' id=ipv6.id %}
|
{% history_button ipv6 %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
{% if machines_list.paginator %}
|
{% if machines_list.paginator %}
|
||||||
|
@ -56,7 +57,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_create Interface machine.id %}
|
{% can_create Interface machine.id %}
|
||||||
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %}
|
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %}
|
{% history_button machine %}
|
||||||
{% can_delete machine %}
|
{% can_delete machine %}
|
||||||
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
|
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
|
@ -127,7 +128,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='interface' id=interface.id %}
|
{% history_button interface %}
|
||||||
{% can_delete interface %}
|
{% can_delete interface %}
|
||||||
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
|
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -40,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit type %}
|
{% can_edit type %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='machinetype' id=type.id %}
|
{% history_button type %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit mx %}
|
{% can_edit mx %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='mx' id=mx.id %}
|
{% history_button mx %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -41,12 +43,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ nas.nas_type }}</td>
|
<td>{{ nas.nas_type }}</td>
|
||||||
<td>{{ nas.machine_type }}</td>
|
<td>{{ nas.machine_type }}</td>
|
||||||
<td>{{ nas.port_access_mode }}</td>
|
<td>{{ nas.port_access_mode }}</td>
|
||||||
<td>{{ nas.autocapture_mac }}</td>
|
<td>{{ nas.autocapture_mac|tick }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% can_edit nas %}
|
{% can_edit nas %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='nas' id=nas.id %}
|
{% history_button nas %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -41,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit ns %}
|
{% can_edit ns %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='ns' id=ns.id %}
|
{% history_button ns %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -22,6 +22,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -37,8 +39,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ server.service }}</td>
|
<td>{{ server.service }}</td>
|
||||||
<td>{{ server.server }}</td>
|
<td>{{ server.server }}</td>
|
||||||
<td>{{ server.last_regen }}</td>
|
<td>{{ server.last_regen }}</td>
|
||||||
<td>{{ server.asked_regen }}</td>
|
<td>{{ server.asked_regen| tick }}</td>
|
||||||
<td>{{ server.need_regen }}</td>
|
<td>{{ server.need_regen | tick }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -45,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit service %}
|
{% can_edit service %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='service' id=service.id %}
|
{% history_button service %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -49,7 +50,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit soa %}
|
{% can_edit soa %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %}
|
{% history_button soa %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -53,7 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit srv %}
|
{% can_edit srv %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='srv' id=srv.id %}
|
{% history_button srv %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -41,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit txt %}
|
{% can_edit txt %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %}
|
{% history_button txt %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
|
@ -45,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit vlan %}
|
{% can_edit vlan %}
|
||||||
{% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %}
|
{% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='machines:history' name='vlan' id=vlan.id %}
|
{% history_button vlan %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -61,6 +61,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-txt' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement TXT</a>
|
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-txt' %}"><i class="fa fa-trash"></i> Supprimer un enregistrement TXT</a>
|
||||||
{% include "machines/aff_txt.html" with txt_list=txt_list %}
|
{% include "machines/aff_txt.html" with txt_list=txt_list %}
|
||||||
|
|
||||||
|
<h2>DNAME records</h2>
|
||||||
|
{% can_create DName %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-dname' %}">
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add a DNAME record" %}
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
|
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-dname' %}">
|
||||||
|
<i class="fa fa-trash"></i> {% trans "Delete DNAME records" %}
|
||||||
|
</a>
|
||||||
|
{% include "machines/aff_dname.html" with dname_list=dname_list %}
|
||||||
|
|
||||||
<h2>Liste des enregistrements SRV</h2>
|
<h2>Liste des enregistrements SRV</h2>
|
||||||
{% can_create Srv %}
|
{% can_create Srv %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-srv' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement SRV</a>
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-srv' %}"><i class="fa fa-plus"></i> Ajouter un enregistrement SRV</a>
|
||||||
|
|
|
@ -57,6 +57,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if txtform %}
|
{% if txtform %}
|
||||||
{% bootstrap_form_errors txtform %}
|
{% bootstrap_form_errors txtform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if dnameform %}
|
||||||
|
{% bootstrap_form_errors dnameform %}
|
||||||
|
{% endif %}
|
||||||
{% if srvform %}
|
{% if srvform %}
|
||||||
{% bootstrap_form_errors srvform %}
|
{% bootstrap_form_errors srvform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -122,6 +125,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<h3>Enregistrement TXT</h3>
|
<h3>Enregistrement TXT</h3>
|
||||||
{% bootstrap_form txtform %}
|
{% bootstrap_form txtform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if dnameform %}
|
||||||
|
<h3>DNAME record</h3>
|
||||||
|
{% bootstrap_form dnameform %}
|
||||||
|
{% endif %}
|
||||||
{% if srvform %}
|
{% if srvform %}
|
||||||
<h3>Enregistrement SRV</h3>
|
<h3>Enregistrement SRV</h3>
|
||||||
{% massive_bootstrap_form srvform 'target' %}
|
{% massive_bootstrap_form srvform 'target' %}
|
||||||
|
|
|
@ -27,7 +27,6 @@ The defined URLs for the Cotisations app
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
import re2o
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -74,6 +73,9 @@ urlpatterns = [
|
||||||
url(r'^add_txt/$', views.add_txt, name='add-txt'),
|
url(r'^add_txt/$', views.add_txt, name='add-txt'),
|
||||||
url(r'^edit_txt/(?P<txtid>[0-9]+)$', views.edit_txt, name='edit-txt'),
|
url(r'^edit_txt/(?P<txtid>[0-9]+)$', views.edit_txt, name='edit-txt'),
|
||||||
url(r'^del_txt/$', views.del_txt, name='del-txt'),
|
url(r'^del_txt/$', views.del_txt, name='del-txt'),
|
||||||
|
url(r'^add_dname/$', views.add_dname, name='add-dname'),
|
||||||
|
url(r'^edit_dname/(?P<dnameid>[0-9]+)$', views.edit_dname, name='edit-dname'),
|
||||||
|
url(r'^del_dname/$', views.del_dname, name='del-dname'),
|
||||||
url(r'^add_ns/$', views.add_ns, name='add-ns'),
|
url(r'^add_ns/$', views.add_ns, name='add-ns'),
|
||||||
url(r'^edit_ns/(?P<nsid>[0-9]+)$', views.edit_ns, name='edit-ns'),
|
url(r'^edit_ns/(?P<nsid>[0-9]+)$', views.edit_ns, name='edit-ns'),
|
||||||
url(r'^del_ns/$', views.del_ns, name='del-ns'),
|
url(r'^del_ns/$', views.del_ns, name='del-ns'),
|
||||||
|
@ -119,10 +121,6 @@ urlpatterns = [
|
||||||
url(r'^edit_nas/(?P<nasid>[0-9]+)$', views.edit_nas, name='edit-nas'),
|
url(r'^edit_nas/(?P<nasid>[0-9]+)$', views.edit_nas, name='edit-nas'),
|
||||||
url(r'^del_nas/$', views.del_nas, name='del-nas'),
|
url(r'^del_nas/$', views.del_nas, name='del-nas'),
|
||||||
url(r'^index_nas/$', views.index_nas, name='index-nas'),
|
url(r'^index_nas/$', views.index_nas, name='index-nas'),
|
||||||
url(r'history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
|
|
||||||
re2o.views.history,
|
|
||||||
name='history',
|
|
||||||
kwargs={'application': 'machines'}),
|
|
||||||
url(r'^$', views.index, name='index'),
|
url(r'^$', views.index, name='index'),
|
||||||
url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'),
|
url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'),
|
||||||
url(r'^rest/regen-achieved/$',
|
url(r'^rest/regen-achieved/$',
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
# se veut agnostique au réseau considéré, de manière à être installable en
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
#
|
#
|
||||||
# Copyright © 2017 Gabriel Détraz
|
# Copyright © 2016-2018 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
# Copyright © 2017 Maël Kervella
|
# Copyright © 2017-2018 Maël Kervella
|
||||||
|
# Copyright © 2018 Charlie Jacomme
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -93,6 +94,8 @@ from .forms import (
|
||||||
DelNsForm,
|
DelNsForm,
|
||||||
TxtForm,
|
TxtForm,
|
||||||
DelTxtForm,
|
DelTxtForm,
|
||||||
|
DNameForm,
|
||||||
|
DelDNameForm,
|
||||||
MxForm,
|
MxForm,
|
||||||
DelMxForm,
|
DelMxForm,
|
||||||
VlanForm,
|
VlanForm,
|
||||||
|
@ -122,6 +125,7 @@ from .models import (
|
||||||
Vlan,
|
Vlan,
|
||||||
Nas,
|
Nas,
|
||||||
Txt,
|
Txt,
|
||||||
|
DName,
|
||||||
Srv,
|
Srv,
|
||||||
OuverturePortList,
|
OuverturePortList,
|
||||||
OuverturePort,
|
OuverturePort,
|
||||||
|
@ -815,6 +819,63 @@ def del_ns(request, instances):
|
||||||
request
|
request
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_create(DName)
|
||||||
|
def add_dname(request):
|
||||||
|
""" View used to add a DName object """
|
||||||
|
dname = DNameForm(request.POST or None)
|
||||||
|
if dname.is_valid():
|
||||||
|
dname.save()
|
||||||
|
messages.success(request, "This DNAME record has been added")
|
||||||
|
return redirect(reverse('machines:index-extension'))
|
||||||
|
return form(
|
||||||
|
{'dnameform': dname, 'action_name': "Create"},
|
||||||
|
'machines/machine.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(DName)
|
||||||
|
def edit_dname(request, dname_instance, **_kwargs):
|
||||||
|
""" View used to edit a DName object """
|
||||||
|
dname = DNameForm(request.POST or None, instance=dname_instance)
|
||||||
|
if dname.is_valid():
|
||||||
|
if dname.changed_data:
|
||||||
|
dname.save()
|
||||||
|
messages.success(request, "DName successfully edited")
|
||||||
|
return redirect(reverse('machines:index-extension'))
|
||||||
|
return form(
|
||||||
|
{'dnameform': dname, 'action_name': "Edit"},
|
||||||
|
'machines/machine.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_delete_set(DName)
|
||||||
|
def del_dname(request, instances):
|
||||||
|
""" View used to delete a DName object """
|
||||||
|
dname = DelDNameForm(request.POST or None, instances=instances)
|
||||||
|
if dname.is_valid():
|
||||||
|
dname_dels = dname.cleaned_data['dname']
|
||||||
|
for dname_del in dname_dels:
|
||||||
|
try:
|
||||||
|
dname_del.delete()
|
||||||
|
messages.success(request,
|
||||||
|
"The DNAME %s has been deleted" % dname_del)
|
||||||
|
except ProtectedError:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"The DNAME %s can not be deleted" % dname_del
|
||||||
|
)
|
||||||
|
return redirect(reverse('machines:index-extension'))
|
||||||
|
return form(
|
||||||
|
{'dnameform': dname, 'action_name': 'Delete'},
|
||||||
|
'machines/machine.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_create(Txt)
|
@can_create(Txt)
|
||||||
|
@ -1272,7 +1333,7 @@ def index_nas(request):
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_view_all(SOA, Mx, Ns, Txt, Srv, Extension)
|
@can_view_all(SOA, Mx, Ns, Txt, DName, Srv, Extension)
|
||||||
def index_extension(request):
|
def index_extension(request):
|
||||||
""" View displaying the list of existing extensions, the list of
|
""" View displaying the list of existing extensions, the list of
|
||||||
existing SOA records, the list of existing MX records , the list of
|
existing SOA records, the list of existing MX records , the list of
|
||||||
|
@ -1292,6 +1353,7 @@ def index_extension(request):
|
||||||
.select_related('zone')
|
.select_related('zone')
|
||||||
.select_related('ns__extension'))
|
.select_related('ns__extension'))
|
||||||
txt_list = Txt.objects.all().select_related('zone')
|
txt_list = Txt.objects.all().select_related('zone')
|
||||||
|
dname_list = DName.objects.all().select_related('zone')
|
||||||
srv_list = (Srv.objects
|
srv_list = (Srv.objects
|
||||||
.all()
|
.all()
|
||||||
.select_related('extension')
|
.select_related('extension')
|
||||||
|
@ -1305,6 +1367,7 @@ def index_extension(request):
|
||||||
'mx_list': mx_list,
|
'mx_list': mx_list,
|
||||||
'ns_list': ns_list,
|
'ns_list': ns_list,
|
||||||
'txt_list': txt_list,
|
'txt_list': txt_list,
|
||||||
|
'dname_list': dname_list,
|
||||||
'srv_list': srv_list
|
'srv_list': srv_list
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,7 @@ from .models import (
|
||||||
Service
|
Service
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EditOptionalUserForm(ModelForm):
|
class EditOptionalUserForm(ModelForm):
|
||||||
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
|
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -54,13 +55,6 @@ class EditOptionalUserForm(ModelForm):
|
||||||
self.fields['is_tel_mandatory'].label = (
|
self.fields['is_tel_mandatory'].label = (
|
||||||
'Exiger un numéro de téléphone'
|
'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'
|
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 __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import preferences.aes_field
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
@ -16,7 +19,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assooption',
|
model_name='assooption',
|
||||||
name='payment_pass',
|
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(
|
migrations.AlterField(
|
||||||
model_name='assooption',
|
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.dispatch import receiver
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
import cotisations.models
|
|
||||||
import machines.models
|
import machines.models
|
||||||
from re2o.mixins import AclMixin
|
from re2o.mixins import AclMixin
|
||||||
|
|
||||||
from .aes_field import AESEncryptedField
|
|
||||||
|
|
||||||
|
|
||||||
class PreferencesModel(models.Model):
|
class PreferencesModel(models.Model):
|
||||||
""" Base object for the Preferences objects
|
""" Base object for the Preferences objects
|
||||||
|
@ -67,22 +64,6 @@ class OptionalUser(AclMixin, PreferencesModel):
|
||||||
PRETTY_NAME = "Options utilisateur"
|
PRETTY_NAME = "Options utilisateur"
|
||||||
|
|
||||||
is_tel_mandatory = models.BooleanField(default=True)
|
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)
|
gpg_fingerprint = models.BooleanField(default=True)
|
||||||
all_can_create_club = models.BooleanField(
|
all_can_create_club = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -108,14 +89,6 @@ class OptionalUser(AclMixin, PreferencesModel):
|
||||||
("view_optionaluser", "Peut voir les options de l'user"),
|
("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)
|
@receiver(post_save, sender=OptionalUser)
|
||||||
def optionaluser_post_save(**kwargs):
|
def optionaluser_post_save(**kwargs):
|
||||||
|
@ -294,25 +267,6 @@ class AssoOption(AclMixin, PreferencesModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=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(
|
description = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
|
@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -43,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% can_edit service%}
|
{% can_edit service%}
|
||||||
{% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %}
|
{% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% include 'buttons/history.html' with href='preferences:history' name='service' id=service.id %}
|
{% history_button service %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
49
preferences/templates/preferences/display_preferences.html
Normal file → Executable file
49
preferences/templates/preferences/display_preferences.html
Normal file → Executable file
|
@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
{% block title %}Création et modification des préférences{% endblock %}
|
{% block title %}Création et modification des préférences{% endblock %}
|
||||||
|
|
||||||
|
@ -39,39 +40,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Téléphone obligatoirement requis</th>
|
<th>Téléphone obligatoirement requis</th>
|
||||||
<td>{{ useroptions.is_tel_mandatory }}</td>
|
<td>{{ useroptions.is_tel_mandatory|tick }}</td>
|
||||||
<th>Activation du solde pour les utilisateurs</th>
|
<th>Auto inscription</th>
|
||||||
<td>{{ useroptions.user_solde }}</td>
|
<td>{{ useroptions.self_adhesion|tick }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Champ gpg fingerprint</th>
|
<th>Champ gpg fingerprint</th>
|
||||||
<td>{{ useroptions.gpg_fingerprint }}</td>
|
<td>{{ useroptions.gpg_fingerprint|tick }}</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>
|
|
||||||
</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>
|
|
||||||
<td>{{ useroptions.self_adhesion }}</td>
|
|
||||||
<th>Shell par défaut des utilisateurs</th>
|
<th>Shell par défaut des utilisateurs</th>
|
||||||
<td>{{ useroptions.shell_default }}</td>
|
<td>{{ useroptions.shell_default }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Creations d'adhérents par tous</th>
|
||||||
|
<td>{{ useroptions.all_can_create_adherent|tick }}</td>
|
||||||
|
<th>Creations de clubs par tous</th>
|
||||||
|
<td>{{ useroptions.all_can_create_club|tick }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<h4>Préférences machines</h4>
|
<h4>Préférences machines</h4>
|
||||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
|
@ -82,7 +69,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Mot de passe par machine</th>
|
<th>Mot de passe par machine</th>
|
||||||
<td>{{ machineoptions.password_machine }}</td>
|
<td>{{ machineoptions.password_machine|tick }}</td>
|
||||||
<th>Machines/interfaces autorisées par utilisateurs</th>
|
<th>Machines/interfaces autorisées par utilisateurs</th>
|
||||||
<td>{{ machineoptions.max_lambdauser_interfaces }}</td>
|
<td>{{ machineoptions.max_lambdauser_interfaces }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -94,7 +81,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Creation de machines</th>
|
<th>Creation de machines</th>
|
||||||
<td>{{ machineoptions.create_machine }}</td>
|
<td>{{ machineoptions.create_machine|tick }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<h4>Préférences topologie</h4>
|
<h4>Préférences topologie</h4>
|
||||||
|
@ -185,12 +172,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<tr>
|
<tr>
|
||||||
<th>Objet utilisateur de l'association</th>
|
<th>Objet utilisateur de l'association</th>
|
||||||
<td>{{ assooptions.utilisateur_asso }}</td>
|
<td>{{ assooptions.utilisateur_asso }}</td>
|
||||||
<th>Moyen de paiement automatique</th>
|
|
||||||
<td>{{ assooptions.payment }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Description de l'association</th>
|
<th>Description de l'association</th>
|
||||||
<td colspan="3">{{ assooptions.description | safe }}</td>
|
<td>{{ assooptions.description | safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
19
preferences/templatetags/__init__.py
Normal file
19
preferences/templatetags/__init__.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#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 Maël Kervella
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
@ -27,7 +27,6 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
import re2o
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,11 +73,5 @@ urlpatterns = [
|
||||||
name='edit-service'
|
name='edit-service'
|
||||||
),
|
),
|
||||||
url(r'^del_services/$', views.del_services, name='del-services'),
|
url(r'^del_services/$', views.del_services, name='del-services'),
|
||||||
url(
|
|
||||||
r'^history/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$',
|
|
||||||
re2o.views.history,
|
|
||||||
name='history',
|
|
||||||
kwargs={'application': 'preferences'},
|
|
||||||
),
|
|
||||||
url(r'^$', views.display_options, name='display-options'),
|
url(r'^$', views.display_options, name='display-options'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
# Copyright © 2018 Maël Kervella
|
# Copyright © 2018 Maël Kervella
|
||||||
|
# Copyright © 2018 Hugo Levy-Falk
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# 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.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
# 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
|
Module defining a AESEncryptedField object that can be used in forms
|
||||||
to handle the use of properly encrypting and decrypting AES keys
|
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 Crypto.Cipher import AES
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
EOD = '`%EofD%`' # This should be something that will not occur in strings
|
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]
|
return ss.split(bytes(EOD, 'utf-8'))[0]
|
||||||
|
|
||||||
|
|
||||||
|
class AESEncryptedFormField(forms.CharField):
|
||||||
|
widget = forms.PasswordInput(render_value=True)
|
||||||
|
|
||||||
|
|
||||||
class AESEncryptedField(models.CharField):
|
class AESEncryptedField(models.CharField):
|
||||||
""" A Field that can be used in forms for adding the support
|
""" A Field that can be used in forms for adding the support
|
||||||
of AES ecnrypted fields """
|
of AES ecnrypted fields """
|
||||||
|
|
||||||
def save_form_data(self, instance, data):
|
def save_form_data(self, instance, data):
|
||||||
setattr(instance, self.name,
|
setattr(instance, self.name, binascii.b2a_base64(
|
||||||
binascii.b2a_base64(encrypt(settings.AES_KEY, data)))
|
encrypt(settings.AES_KEY, data)).decode('utf-8'))
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
try:
|
||||||
return decrypt(settings.AES_KEY,
|
return decrypt(settings.AES_KEY,
|
||||||
binascii.a2b_base64(value)).decode('utf-8')
|
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):
|
def get_prep_value(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -85,4 +101,9 @@ class AESEncryptedField(models.CharField):
|
||||||
return binascii.b2a_base64(encrypt(
|
return binascii.b2a_base64(encrypt(
|
||||||
settings.AES_KEY,
|
settings.AES_KEY,
|
||||||
value
|
value
|
||||||
))
|
)).decode('utf-8')
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
defaults = {'form_class': AESEncryptedFormField}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super().formfield(**defaults)
|
|
@ -21,8 +21,10 @@
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
"""Fonction de context, variables renvoyées à toutes les vues"""
|
"""Fonction de context, variables renvoyées à toutes les vues"""
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
from preferences.models import GeneralOption, OptionalMachine
|
from preferences.models import GeneralOption, OptionalMachine
|
||||||
|
@ -47,3 +49,12 @@ def context_user(request):
|
||||||
'name_website': GeneralOption.get_cached_value('site_name'),
|
'name_website': GeneralOption.get_cached_value('site_name'),
|
||||||
'ipv6_enabled': OptionalMachine.get_cached_value('ipv6'),
|
'ipv6_enabled': OptionalMachine.get_cached_value('ipv6'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def date_now(request):
|
||||||
|
"""Add the current date in the context for quick informations and
|
||||||
|
comparisons"""
|
||||||
|
return {
|
||||||
|
'now_aware': datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
'now_naive': datetime.datetime.now()
|
||||||
|
}
|
||||||
|
|
|
@ -125,6 +125,7 @@ TEMPLATES = [
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
're2o.context_processors.context_user',
|
're2o.context_processors.context_user',
|
||||||
|
're2o.context_processors.date_now',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -74,83 +74,42 @@ import sys
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.template.base import Node, NodeList
|
from django.template.base import Node, NodeList
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
import cotisations
|
|
||||||
import machines
|
|
||||||
import preferences
|
|
||||||
import topologie
|
|
||||||
import users
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
MODEL_NAME = {
|
|
||||||
# cotisations
|
|
||||||
'Facture': cotisations.models.Facture,
|
|
||||||
'Vente': cotisations.models.Vente,
|
|
||||||
'Article': cotisations.models.Article,
|
|
||||||
'Banque': cotisations.models.Banque,
|
|
||||||
'Paiement': cotisations.models.Paiement,
|
|
||||||
'Cotisation': cotisations.models.Cotisation,
|
|
||||||
# machines
|
|
||||||
'Machine': machines.models.Machine,
|
|
||||||
'MachineType': machines.models.MachineType,
|
|
||||||
'IpType': machines.models.IpType,
|
|
||||||
'Vlan': machines.models.Vlan,
|
|
||||||
'Nas': machines.models.Nas,
|
|
||||||
'SOA': machines.models.SOA,
|
|
||||||
'Extension': machines.models.Extension,
|
|
||||||
'Mx': machines.models.Mx,
|
|
||||||
'Ns': machines.models.Ns,
|
|
||||||
'Txt': machines.models.Txt,
|
|
||||||
'Srv': machines.models.Srv,
|
|
||||||
'Interface': machines.models.Interface,
|
|
||||||
'Domain': machines.models.Domain,
|
|
||||||
'IpList': machines.models.IpList,
|
|
||||||
'Ipv6List': machines.models.Ipv6List,
|
|
||||||
'machines.Service': machines.models.Service,
|
|
||||||
'Service_link': machines.models.Service_link,
|
|
||||||
'OuverturePortList': machines.models.OuverturePortList,
|
|
||||||
'OuverturePort': machines.models.OuverturePort,
|
|
||||||
# preferences
|
|
||||||
'OptionalUser': preferences.models.OptionalUser,
|
|
||||||
'OptionalMachine': preferences.models.OptionalMachine,
|
|
||||||
'OptionalTopologie': preferences.models.OptionalTopologie,
|
|
||||||
'GeneralOption': preferences.models.GeneralOption,
|
|
||||||
'preferences.Service': preferences.models.Service,
|
|
||||||
'AssoOption': preferences.models.AssoOption,
|
|
||||||
'MailMessageOption': preferences.models.MailMessageOption,
|
|
||||||
# topologie
|
|
||||||
'Stack': topologie.models.Stack,
|
|
||||||
'Switch': topologie.models.Switch,
|
|
||||||
'AccessPoint': topologie.models.AccessPoint,
|
|
||||||
'ModelSwitch': topologie.models.ModelSwitch,
|
|
||||||
'ConstructorSwitch': topologie.models.ConstructorSwitch,
|
|
||||||
'Port': topologie.models.Port,
|
|
||||||
'Room': topologie.models.Room,
|
|
||||||
'Building': topologie.models.Building,
|
|
||||||
'SwitchBay': topologie.models.SwitchBay,
|
|
||||||
'PortProfile': topologie.models.PortProfile,
|
|
||||||
# users
|
|
||||||
'User': users.models.User,
|
|
||||||
'Adherent': users.models.Adherent,
|
|
||||||
'Club': users.models.Club,
|
|
||||||
'ServiceUser': users.models.ServiceUser,
|
|
||||||
'School': users.models.School,
|
|
||||||
'ListRight': users.models.ListRight,
|
|
||||||
'ListShell': users.models.ListShell,
|
|
||||||
'Ban': users.models.Ban,
|
|
||||||
'Whitelist': users.models.Whitelist,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_model(model_name):
|
def get_model(model_name):
|
||||||
"""Retrieve the model object from its name"""
|
"""Retrieve the model object from its name"""
|
||||||
|
splitted = model_name.split('.')
|
||||||
|
if len(splitted) > 1:
|
||||||
try:
|
try:
|
||||||
return MODEL_NAME[model_name]
|
app_label, name = splitted
|
||||||
except KeyError:
|
except ValueError:
|
||||||
|
raise template.TemplateSyntaxError(
|
||||||
|
"%r is an inconsistent model name" % model_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app_label, name = None, splitted[0]
|
||||||
|
try:
|
||||||
|
if app_label is not None:
|
||||||
|
content_type = ContentType.objects.get(
|
||||||
|
model=name.lower(),
|
||||||
|
app_label=app_label
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content_type = ContentType.objects.get(model=name.lower())
|
||||||
|
except ContentType.DoesNotExist:
|
||||||
raise template.TemplateSyntaxError(
|
raise template.TemplateSyntaxError(
|
||||||
"%r is not a valid model for an acl tag" % model_name
|
"%r is not a valid model for an acl tag" % model_name
|
||||||
)
|
)
|
||||||
|
except ContentType.MultipleObjectsReturned:
|
||||||
|
raise template.TemplateSyntaxError(
|
||||||
|
"More than one model found for %r. Try with `app.model`."
|
||||||
|
% model_name
|
||||||
|
)
|
||||||
|
return content_type.model_class()
|
||||||
|
|
||||||
|
|
||||||
def get_callback(tag_name, obj=None):
|
def get_callback(tag_name, obj=None):
|
||||||
|
|
39
re2o/templatetags/design.py
Normal file
39
re2o/templatetags/design.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
#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 Maël Kervella
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import conditional_escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(needs_autoescape=False)
|
||||||
|
def tick(valeur, autoescape=False):
|
||||||
|
|
||||||
|
if isinstance(valeur,bool):
|
||||||
|
if valeur == True:
|
||||||
|
result = '<i style="color: #1ECA18;" class="fas fa-check"></i>'
|
||||||
|
else:
|
||||||
|
result = '<i style="color: #D10115;" class="fas fa-times"></i>'
|
||||||
|
return mark_safe(result)
|
||||||
|
|
||||||
|
else: # if the value is not a boolean, display it as if tick was not called
|
||||||
|
return valeur
|
|
@ -50,6 +50,7 @@ from django.contrib.auth import views as auth_views
|
||||||
from .views import index, about_page
|
from .views import index, about_page
|
||||||
|
|
||||||
handler500 = 're2o.views.handler500'
|
handler500 = 're2o.views.handler500'
|
||||||
|
handler404 = 're2o.views.handler404'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', index, name='index'),
|
url(r'^$', index, name='index'),
|
||||||
|
|
127
re2o/views.py
127
re2o/views.py
|
@ -26,33 +26,20 @@ les views
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from itertools import chain
|
|
||||||
import git
|
import git
|
||||||
from reversion.models import Version
|
|
||||||
|
|
||||||
from django.http import Http404
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
|
||||||
from django.shortcuts import render, redirect
|
|
||||||
from django.template.context_processors import csrf
|
from django.template.context_processors import csrf
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
|
|
||||||
import preferences
|
|
||||||
from preferences.models import (
|
from preferences.models import (
|
||||||
Service,
|
Service,
|
||||||
GeneralOption,
|
|
||||||
AssoOption,
|
AssoOption,
|
||||||
HomeOption
|
HomeOption
|
||||||
)
|
)
|
||||||
import users
|
|
||||||
import cotisations
|
|
||||||
import topologie
|
|
||||||
import machines
|
|
||||||
|
|
||||||
from .utils import re2o_paginator
|
|
||||||
from .contributors import CONTRIBUTORS
|
from .contributors import CONTRIBUTORS
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,113 +68,6 @@ def index(request):
|
||||||
}, 're2o/index.html', request)
|
}, 're2o/index.html', request)
|
||||||
|
|
||||||
|
|
||||||
#: Binding the corresponding char sequence of history url to re2o models.
|
|
||||||
HISTORY_BIND = {
|
|
||||||
'users': {
|
|
||||||
'user': users.models.User,
|
|
||||||
'ban': users.models.Ban,
|
|
||||||
'whitelist': users.models.Whitelist,
|
|
||||||
'school': users.models.School,
|
|
||||||
'listright': users.models.ListRight,
|
|
||||||
'serviceuser': users.models.ServiceUser,
|
|
||||||
'listshell': users.models.ListShell,
|
|
||||||
},
|
|
||||||
'preferences': {
|
|
||||||
'service': preferences.models.Service,
|
|
||||||
},
|
|
||||||
'cotisations': {
|
|
||||||
'facture': cotisations.models.Facture,
|
|
||||||
'article': cotisations.models.Article,
|
|
||||||
'paiement': cotisations.models.Paiement,
|
|
||||||
'banque': cotisations.models.Banque,
|
|
||||||
},
|
|
||||||
'topologie': {
|
|
||||||
'switch': topologie.models.Switch,
|
|
||||||
'port': topologie.models.Port,
|
|
||||||
'room': topologie.models.Room,
|
|
||||||
'stack': topologie.models.Stack,
|
|
||||||
'modelswitch': topologie.models.ModelSwitch,
|
|
||||||
'constructorswitch': topologie.models.ConstructorSwitch,
|
|
||||||
'accesspoint': topologie.models.AccessPoint,
|
|
||||||
'switchbay': topologie.models.SwitchBay,
|
|
||||||
'building': topologie.models.Building,
|
|
||||||
'portprofile': topologie.models.PortProfile,
|
|
||||||
},
|
|
||||||
'machines': {
|
|
||||||
'machine': machines.models.Machine,
|
|
||||||
'interface': machines.models.Interface,
|
|
||||||
'domain': machines.models.Domain,
|
|
||||||
'machinetype': machines.models.MachineType,
|
|
||||||
'iptype': machines.models.IpType,
|
|
||||||
'extension': machines.models.Extension,
|
|
||||||
'soa': machines.models.SOA,
|
|
||||||
'mx': machines.models.Mx,
|
|
||||||
'txt': machines.models.Txt,
|
|
||||||
'srv': machines.models.Srv,
|
|
||||||
'ns': machines.models.Ns,
|
|
||||||
'service': machines.models.Service,
|
|
||||||
'vlan': machines.models.Vlan,
|
|
||||||
'nas': machines.models.Nas,
|
|
||||||
'ipv6list': machines.models.Ipv6List,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def history(request, application, object_name, object_id):
|
|
||||||
"""Render history for a model.
|
|
||||||
|
|
||||||
The model is determined using the `HISTORY_BIND` dictionnary if none is
|
|
||||||
found, raises a Http404. The view checks if the user is allowed to see the
|
|
||||||
history using the `can_view` method of the model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: The request sent by the user.
|
|
||||||
object_name: Name of the model.
|
|
||||||
object_id: Id of the object you want to acces history.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The rendered page of history if access is granted, else the user is
|
|
||||||
redirected to their profile page, with an error message.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Http404: This kind of models doesn't have history.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
model = HISTORY_BIND[application][object_name]
|
|
||||||
except KeyError:
|
|
||||||
raise Http404(u"Il n'existe pas d'historique pour ce modèle.")
|
|
||||||
object_name_id = object_name + 'id'
|
|
||||||
kwargs = {object_name_id: object_id}
|
|
||||||
try:
|
|
||||||
instance = model.get_instance(**kwargs)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
messages.error(request, u"Entrée inexistante")
|
|
||||||
return redirect(reverse(
|
|
||||||
'users:profil',
|
|
||||||
kwargs={'userid': str(request.user.id)}
|
|
||||||
))
|
|
||||||
can, msg = instance.can_view(request.user)
|
|
||||||
if not can:
|
|
||||||
messages.error(request, msg or "Vous ne pouvez pas accéder à ce menu")
|
|
||||||
return redirect(reverse(
|
|
||||||
'users:profil',
|
|
||||||
kwargs={'userid': str(request.user.id)}
|
|
||||||
))
|
|
||||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
|
||||||
reversions = Version.objects.get_for_object(instance)
|
|
||||||
if hasattr(instance, 'linked_objects'):
|
|
||||||
for related_object in chain(instance.linked_objects()):
|
|
||||||
reversions = (reversions |
|
|
||||||
Version.objects.get_for_object(related_object))
|
|
||||||
reversions = re2o_paginator(request, reversions, pagination_number)
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
're2o/history.html',
|
|
||||||
{'reversions': reversions, 'object': instance}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cache_page(7 * 24 * 60 * 60)
|
@cache_page(7 * 24 * 60 * 60)
|
||||||
def about_page(request):
|
def about_page(request):
|
||||||
""" The view for the about page.
|
""" The view for the about page.
|
||||||
|
@ -230,3 +110,8 @@ def about_page(request):
|
||||||
def handler500(request):
|
def handler500(request):
|
||||||
"""The handler view for a 500 error"""
|
"""The handler view for a 500 error"""
|
||||||
return render(request, 'errors/500.html')
|
return render(request, 'errors/500.html')
|
||||||
|
|
||||||
|
|
||||||
|
def handler404(request):
|
||||||
|
"""The handler view for a 404 error"""
|
||||||
|
return render(request, 'errors/404.html')
|
||||||
|
|
|
@ -108,3 +108,9 @@ footer a {
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make modal wider on wide screens */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.modal-dialog {
|
||||||
|
width: 1000px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,7 +21,8 @@ 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.,
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url href name id %}">
|
{% load i18n %}
|
||||||
<i class="fa fa-history"></i>
|
<a {% if class%}class="btn btn-info btn-sm"{% endif %} role="button" title="{% trans 'History' %}" href="{% url 'logs:history' application name id %}">
|
||||||
|
<i class="fa fa-history"></i> {% if text %}{% trans 'History' %}{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
227
templates/errors/404.html
Normal file
227
templates/errors/404.html
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2018 Hugo Levy-Falk
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head prefix="og: http://ogp.me/ns#">
|
||||||
|
<meta property="og:title" content="Re2o" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="{{ request.scheme }}://{{ request.get_host }}/" />
|
||||||
|
<meta property="og:image" content="{% static 'images/logo_re2o.svg' %}"/>
|
||||||
|
<meta property="og:image:type" content="image/svg"/>
|
||||||
|
<meta property="og:image:alt" content="The Re2o logo"/>
|
||||||
|
<meta property="og:description" content="Site de gestion de réseau supporté par FedeRez." />
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="shortcut icon" type="image/svg" href="{% static 'images/logo_re2o.svg' %}">
|
||||||
|
<title>404, Page not Found</title>
|
||||||
|
<script src="/javascript/jquery/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var snake = [{x:0,y:0,vx:1,vy:0}];
|
||||||
|
var bonus = [];
|
||||||
|
var lost = false;
|
||||||
|
var grid = 20;
|
||||||
|
var score = 0;
|
||||||
|
|
||||||
|
function update_snake() {
|
||||||
|
var l = snake.length;
|
||||||
|
var c = document.getElementById("myCanvas");
|
||||||
|
var width = c.width;
|
||||||
|
var height = c.height;
|
||||||
|
var last_case = {
|
||||||
|
x:snake[l-1].x,
|
||||||
|
y:snake[l-1].y,
|
||||||
|
vx:snake[l-1].vx,
|
||||||
|
vy:snake[l-1].vy
|
||||||
|
};
|
||||||
|
for(var i=l-1; i>=0; --i){
|
||||||
|
if(i == 0)
|
||||||
|
{
|
||||||
|
var m = bonus.length;
|
||||||
|
var remove = -1;
|
||||||
|
for(var j=0; j<m; ++j)
|
||||||
|
{
|
||||||
|
if((bonus[j].x == snake[i].x) && (bonus[j].y == snake[i].y))
|
||||||
|
{
|
||||||
|
remove = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(remove >= 0){
|
||||||
|
bonus.splice(remove, 1);
|
||||||
|
snake.push(last_case);
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if((i > 0) && (snake[i].x == snake[0].x) && (snake[i].y == snake[0].y))
|
||||||
|
{
|
||||||
|
lost = true;
|
||||||
|
}
|
||||||
|
snake[i].x = (snake[i].x + snake[i].vx * grid + width)%width;
|
||||||
|
snake[i].y = (snake[i].y + snake[i].vy * grid + height)%height;
|
||||||
|
if(i>0)
|
||||||
|
{
|
||||||
|
snake[i].vx = snake[i-1].vx;
|
||||||
|
snake[i].vy = snake[i-1].vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_snake() {
|
||||||
|
var l = snake.length;
|
||||||
|
var c = document.getElementById("myCanvas");
|
||||||
|
if(c.getContext) {
|
||||||
|
var ctx = c.getContext("2d");
|
||||||
|
for(var i=0; i<l; ++i){
|
||||||
|
ctx.fillStyle = "#2980b9";
|
||||||
|
ctx.fillRect(snake[i].x, snake[i].y, grid, grid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_bonus() {
|
||||||
|
var l = bonus.length;
|
||||||
|
var ctx = document.getElementById("myCanvas").getContext("2d");
|
||||||
|
for(var i=0; i<l; ++i)
|
||||||
|
{
|
||||||
|
ctx.beginPath();
|
||||||
|
var x = bonus[i].x;
|
||||||
|
var y = bonus[i].y;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x+grid/2, y+grid/2, grid/2, 0, 2 * Math.PI, false);
|
||||||
|
ctx.fillStyle = '#2ecc71';
|
||||||
|
ctx.fill();
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = '#27ae60';
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_score(){
|
||||||
|
var ctx = document.getElementById('myCanvas').getContext('2d');
|
||||||
|
ctx.font = '50px serif';
|
||||||
|
ctx.fillStyle = '#2ecc71';
|
||||||
|
ctx.fillText("{% trans "Score :"%} " + score, 10, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_lost(){
|
||||||
|
var c = document.getElementById("myCanvas");
|
||||||
|
var ctx = c.getContext('2d');
|
||||||
|
ctx.fillStyle = '#2ecc71';
|
||||||
|
ctx.font = '50px serif';
|
||||||
|
ctx.fillText("{% trans "YOU LOST" %}", c.width/2, c.height/2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function update_bonus() {
|
||||||
|
var c = document.getElementById("myCanvas");
|
||||||
|
var width = c.width;
|
||||||
|
var height = c.height;
|
||||||
|
var x = (Math.floor(Math.random() * width / grid))*grid;
|
||||||
|
var y = (Math.floor(Math.random() * height / grid))*grid;
|
||||||
|
bonus.push({x:x, y:y});
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
var c = document.getElementById("myCanvas");
|
||||||
|
var width = c.width;
|
||||||
|
var height = c.height;
|
||||||
|
var ctx = c.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
if(!lost){
|
||||||
|
draw_snake();
|
||||||
|
draw_bonus();
|
||||||
|
draw_score();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
draw_score();
|
||||||
|
draw_lost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_keydown(e) {
|
||||||
|
if(e.which == 37) { // left
|
||||||
|
snake[0].vx = -1;
|
||||||
|
snake[0].vy = 0;
|
||||||
|
}
|
||||||
|
else if(e.which == 38) { // up
|
||||||
|
snake[0].vx = 0;
|
||||||
|
snake[0].vy = -1;
|
||||||
|
}
|
||||||
|
else if(e.which == 39) { // right
|
||||||
|
snake[0].vx = 1;
|
||||||
|
snake[0].vy = 0;
|
||||||
|
}
|
||||||
|
else if(e.which == 40) { // down
|
||||||
|
snake[0].vx = 0;
|
||||||
|
snake[0].vy = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$("html").keydown(on_keydown);
|
||||||
|
window.setInterval(draw, 100);
|
||||||
|
window.setInterval(update_snake, 100);
|
||||||
|
window.setInterval(update_bonus, 3000);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background: #34495e;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
display:block;
|
||||||
|
text-align: center;
|
||||||
|
background: #2c3e50;
|
||||||
|
padding: 1em;
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
color: #ecf0f1;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
a
|
||||||
|
{
|
||||||
|
font-size: x-small;
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
#myCanvas
|
||||||
|
{
|
||||||
|
width:80%;
|
||||||
|
display:block;
|
||||||
|
margin-left:auto;
|
||||||
|
margin-right:auto;
|
||||||
|
height:50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{% trans "Yup, that's a 404 error."%} <a href="/">{% trans "(Go to a known place)"%}</a></h1>
|
||||||
|
<canvas id="myCanvas" width="800px" height="300px" style="border:1px solid #d3d3d3;">
|
||||||
|
{%trans "Your browser does not support the HTML5 canvas tag."%}
|
||||||
|
</canvas>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -89,15 +89,7 @@ class EditPortForm(FormRevMixin, ModelForm):
|
||||||
self.fields['machine_interface'].queryset = (
|
self.fields['machine_interface'].queryset = (
|
||||||
Interface.objects.all().select_related('domain__extension')
|
Interface.objects.all().select_related('domain__extension')
|
||||||
)
|
)
|
||||||
self.fields['related'].queryset = (
|
self.fields['related'].queryset = Port.objects.all().prefetch_related('switch__machine_ptr__interface_set__domain__extension')
|
||||||
Port.objects.all()
|
|
||||||
.prefetch_related(Prefetch(
|
|
||||||
'switch__interface_set',
|
|
||||||
queryset=(Interface.objects
|
|
||||||
.select_related('ipv4__ip_type__extension')
|
|
||||||
.select_related('domain__extension'))
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AddPortForm(FormRevMixin, ModelForm):
|
class AddPortForm(FormRevMixin, ModelForm):
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('topologie', '0029_auto_20171002_0334'),
|
('topologie', '0029_auto_20171002_0334'),
|
||||||
|
('machines', '0049_vlan'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -282,8 +282,12 @@ class Switch(AclMixin, Machine):
|
||||||
""" Returns the 'main' interface of the switch """
|
""" Returns the 'main' interface of the switch """
|
||||||
return self.interface_set.first()
|
return self.interface_set.first()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def get_name(self):
|
||||||
|
return self.name or self.main_interface().domain.name
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.main_interface())
|
return str(self.get_name)
|
||||||
|
|
||||||
|
|
||||||
class ModelSwitch(AclMixin, RevMixin, models.Model):
|
class ModelSwitch(AclMixin, RevMixin, models.Model):
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
{% if ap_list.paginator %}
|
{% if ap_list.paginator %}
|
||||||
|
@ -49,9 +50,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ap.interface_set.first.details}}</td>
|
<td>{{ap.interface_set.first.details}}</td>
|
||||||
<td>{{ap.location}}</td>
|
<td>{{ap.location}}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'accesspoint' ap.pk %}">
|
{% history_button ap %}
|
||||||
<i class="fa fa-history"></i>
|
|
||||||
</a>
|
|
||||||
{% can_edit ap %}
|
{% can_edit ap %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-ap' ap.id %}">
|
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-ap' ap.id %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
|
|
|
@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load acl %}
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
|
||||||
{% if building_list.paginator %}
|
{% if building_list.paginator %}
|
||||||
{% include "pagination.html" with list=building_list %}
|
{% include "pagination.html" with list=building_list %}
|
||||||
|
@ -39,9 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{building.name}}</td>
|
<td>{{building.name}}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'building' building.pk %}">
|
{% history_button building %}
|
||||||
<i class="fa fa-history"></i>
|
|
||||||
</a>
|
|
||||||
{% can_edit building %}
|
{% can_edit building %}
|
||||||
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-building' building.id %}">
|
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'topologie:edit-building' building.id %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue