8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-26 06:32:26 +00:00

Release : 2.7

This commit is contained in:
Hugo LEVY-FALK 2019-02-07 18:48:46 +01:00
commit a8dbe4621f
265 changed files with 11067 additions and 9165 deletions

52
.gitignore vendored
View file

@ -1,8 +1,48 @@
settings_local.py # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.swp *.swp
*.pyc
# Translations
*.mo
*.pot
# Django stuff
*.log
local_settings.py
db.sqlite3
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyCharm project settings
.idea/
# Django statics
static_files/
static/logo/
# re2o specific
settings_local.py
re2o.png re2o.png
__pycache__/* media/
static_files/*
static/logo/*
media/*

View file

@ -150,3 +150,31 @@ On some database engines (postgreSQL) you also need to update the id sequences:
```bash ```bash
python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell
``` ```
## MR 296: Frontend changes
Install fonts-font-awesome
```bash
apt-get -y install fonts-font-awesome
```
Collec new statics
```bash
python3 manage.py collectstatic
```
## MR 391: Document templates and subscription vouchers
Re2o can now use templates for generated invoices. To load default templates run
```bash
./install update
```
Be carefull, you need the proper rights to edit a DocumentTemplate.
Re2o now sends subscription voucher when an invoice is controlled. It uses one
of the templates. You also need to set the name of the president of your association
to be set in your settings.

View file

@ -1,10 +1,10 @@
# Re2o # Re2o
Gnu public license v2.0 GNU public license v2.0
## Avant propos ## Avant propos
Re2o est un logiciel d'administration développé initiallement au rezometz. Il Re2o est un logiciel d'administration développé initialement 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.
@ -31,15 +31,15 @@ Pour cela :
## Fonctionnement général ## Fonctionnement général
Re2o est séparé entre les models, qui sont visible sur le schéma des Re2o est séparé entre les models, qui sont visibles sur le schéma des
dépendances. Il s'agit en réalité des tables sql, et les fields etant les dépendances. Il s'agit en réalité des tables sql, et les fields étant les
colonnes. colonnes.
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django Ceci dit il n'est jamais nécessaire de toucher directement au sql, django
procédant automatiquement à tout cela. procédant automatiquement à tout cela.
On crée donc différents models (user, right pour les droits des users, On crée donc différents models (user, right pour les droits des users,
interfaces, IpList pour l'ensemble des adresses ip, etc) interfaces, IpList pour l'ensemble des adresses ip, etc)
Du coté des forms, il s'agit des formulaire d'édition des models. Il Du coté des forms, il s'agit des formulaires d'édition des models. Il
s'agit de ModelForms django, qui héritent des models très simplement, voir la s'agit de ModelForms django, qui héritent des models très simplement, voir la
documentation django models forms. documentation django models forms.
@ -56,12 +56,20 @@ d'accéder à ces vues, utilisé par re2o-tools.
# Requète en base de donnée # Requète en base de donnée
Pour avoir un shell, il suffit de lancer '''python3 manage.py shell''' Pour avoir un shell, lancer :
Pour charger des objets, example avec User, faire : ```.bash
''' from users.models import User''' python3 manage.py shell
Pour charger les objets django, il suffit de faire User.objects.all() ```
Pour charger des objets (exemple avec User), faire :
```.python
from users.models import User
```
Pour charger les objets django, il suffit de faire `User.objects.all()`
pour tous les users par exemple. pour tous les users par exemple.
Il est ensuite aisé de faire des requètes, par exemple Il est ensuite aisé de faire des requêtes, par exemple
User.objects.filter(pseudo='test') `User.objects.filter(pseudo='test')`
Des exemples et la documentation complète sur les requètes django sont
Des exemples et la documentation complète sur les requêtes django sont
disponible sur le site officiel. disponible sur le site officiel.

View file

@ -26,9 +26,9 @@ done.
""" """
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
def _create_api_permission(): def _create_api_permission():
@ -71,4 +71,5 @@ def can_view(user):
'codename': settings.API_PERMISSION_CODENAME 'codename': settings.API_PERMISSION_CODENAME
} }
can = user.has_perm('%(app_label)s.%(codename)s' % kwargs) can = user.has_perm('%(app_label)s.%(codename)s' % kwargs)
return can, None if can else _("You cannot see this application.") return can, None if can else _("You don't have the right to see this"
" application.")

View file

@ -26,12 +26,14 @@ import datetime
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
class ExpiringTokenAuthentication(TokenAuthentication): class ExpiringTokenAuthentication(TokenAuthentication):
"""Authenticate a user if the provided token is valid and not expired. """Authenticate a user if the provided token is valid and not expired.
""" """
def authenticate_credentials(self, key): def authenticate_credentials(self, key):
"""See base class. Add the verification the token is not expired. """See base class. Add the verification the token is not expired.
""" """
@ -44,6 +46,6 @@ class ExpiringTokenAuthentication(TokenAuthentication):
) )
utc_now = datetime.datetime.now(datetime.timezone.utc) utc_now = datetime.datetime.now(datetime.timezone.utc)
if token.created < utc_now - token_duration: if token.created < utc_now - token_duration:
raise exceptions.AuthenticationFailed(_('Token has expired')) raise exceptions.AuthenticationFailed(_("The token has expired."))
return (token.user, token) return token.user, token

View file

@ -0,0 +1,40 @@
# 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 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.
msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-01-08 23:06+0100\n"
"PO-Revision-Date: 2019-01-07 01:37+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: acl.py:74
msgid "You don't have the right to see this application."
msgstr "Vous n'avez pas le droit de voir cette application."
#: authentication.py:49
msgid "The token has expired."
msgstr "Le jeton a expiré."

View file

@ -24,8 +24,6 @@
from rest_framework import permissions, exceptions from rest_framework import permissions, exceptions
from re2o.acl import can_create, can_edit, can_delete, can_view_all
from . import acl from . import acl
@ -80,7 +78,8 @@ class ACLPermission(permissions.BasePermission):
See the wiki for the syntax of this attribute. See the wiki for the syntax of this attribute.
""" """
def get_required_permissions(self, method, view): @staticmethod
def get_required_permissions(method, view):
"""Build the list of permissions required for the request to be """Build the list of permissions required for the request to be
accepted. accepted.
@ -209,7 +208,8 @@ class AutodetectACLPermission(permissions.BasePermission):
return [perm(obj) for perm in self.perms_obj_map[method]] return [perm(obj) for perm in self.perms_obj_map[method]]
def _queryset(self, view): @staticmethod
def _queryset(view):
return _get_param_in_view(view, 'queryset') return _get_param_in_view(view, 'queryset')
def has_permission(self, request, view): def has_permission(self, request, view):
@ -282,4 +282,3 @@ class AutodetectACLPermission(permissions.BasePermission):
return False return False
return True return True

View file

@ -24,12 +24,12 @@
from collections import OrderedDict from collections import OrderedDict
from django.conf.urls import url, include from django.conf.urls import url
from django.core.urlresolvers import NoReverseMatch from django.core.urlresolvers import NoReverseMatch
from rest_framework import views from rest_framework import views
from rest_framework.routers import DefaultRouter
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.routers import DefaultRouter
from rest_framework.schemas import SchemaGenerator from rest_framework.schemas import SchemaGenerator
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -64,7 +64,8 @@ class AllViewsRouter(DefaultRouter):
name = self.get_default_name(pattern) name = self.get_default_name(pattern)
self.view_registry.append((pattern, view, name)) self.view_registry.append((pattern, view, name))
def get_default_name(self, pattern): @staticmethod
def get_default_name(pattern):
"""Returns the name to use for the route if none was specified. """Returns the name to use for the route if none was specified.
Args: Args:
@ -113,7 +114,8 @@ class AllViewsRouter(DefaultRouter):
_ignore_model_permissions = True _ignore_model_permissions = True
renderer_classes = view_renderers renderer_classes = view_renderers
def get(self, request, *args, **kwargs): @staticmethod
def get(request, *args, **kwargs):
if request.accepted_renderer.media_type in schema_media_types: if request.accepted_renderer.media_type in schema_media_types:
# Return a schema response. # Return a schema response.
schema = schema_generator.get_schema(request) schema = schema_generator.get_schema(request)

View file

@ -30,7 +30,6 @@ import preferences.models as preferences
import topologie.models as topologie import topologie.models as topologie
import users.models as users import users.models as users
# The namespace used for the API. It must match the namespace used in the # The namespace used for the API. It must match the namespace used in the
# urlpatterns to include the API URLs. # urlpatterns to include the API URLs.
API_NAMESPACE = 'api' API_NAMESPACE = 'api'
@ -40,6 +39,7 @@ class NamespacedHRField(serializers.HyperlinkedRelatedField):
"""A `rest_framework.serializers.HyperlinkedRelatedField` subclass to """A `rest_framework.serializers.HyperlinkedRelatedField` subclass to
automatically prefix view names with the API namespace. automatically prefix view names with the API namespace.
""" """
def __init__(self, view_name=None, **kwargs): def __init__(self, view_name=None, **kwargs):
if view_name is not None: if view_name is not None:
view_name = '%s:%s' % (API_NAMESPACE, view_name) view_name = '%s:%s' % (API_NAMESPACE, view_name)
@ -50,6 +50,7 @@ class NamespacedHIField(serializers.HyperlinkedIdentityField):
"""A `rest_framework.serializers.HyperlinkedIdentityField` subclass to """A `rest_framework.serializers.HyperlinkedIdentityField` subclass to
automatically prefix view names with teh API namespace. automatically prefix view names with teh API namespace.
""" """
def __init__(self, view_name=None, **kwargs): def __init__(self, view_name=None, **kwargs):
if view_name is not None: if view_name is not None:
view_name = '%s:%s' % (API_NAMESPACE, view_name) view_name = '%s:%s' % (API_NAMESPACE, view_name)
@ -70,24 +71,33 @@ class NamespacedHMSerializer(serializers.HyperlinkedModelSerializer):
class FactureSerializer(NamespacedHMSerializer): class FactureSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Facture` objects. """Serialize `cotisations.models.Facture` objects.
""" """
class Meta: class Meta:
model = cotisations.Facture model = cotisations.Facture
fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid', fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid',
'control', 'prix_total', 'name', 'api_url') 'control', 'prix_total', 'name', 'api_url')
class BaseInvoiceSerializer(NamespacedHMSerializer):
class Meta:
model = cotisations.BaseInvoice
fields = ('__all__')
class VenteSerializer(NamespacedHMSerializer): class VenteSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Vente` objects. """Serialize `cotisations.models.Vente` objects.
""" """
class Meta: class Meta:
model = cotisations.Vente model = cotisations.Vente
fields = ('facture', 'number', 'name', 'prix', 'duration', fields = ('facture',
'number', 'name', 'prix', 'duration',
'type_cotisation', 'prix_total', 'api_url') 'type_cotisation', 'prix_total', 'api_url')
class ArticleSerializer(NamespacedHMSerializer): class ArticleSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Article` objects. """Serialize `cotisations.models.Article` objects.
""" """
class Meta: class Meta:
model = cotisations.Article model = cotisations.Article
fields = ('name', 'prix', 'duration', 'type_user', fields = ('name', 'prix', 'duration', 'type_user',
@ -97,6 +107,7 @@ class ArticleSerializer(NamespacedHMSerializer):
class BanqueSerializer(NamespacedHMSerializer): class BanqueSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Banque` objects. """Serialize `cotisations.models.Banque` objects.
""" """
class Meta: class Meta:
model = cotisations.Banque model = cotisations.Banque
fields = ('name', 'api_url') fields = ('name', 'api_url')
@ -105,14 +116,16 @@ class BanqueSerializer(NamespacedHMSerializer):
class PaiementSerializer(NamespacedHMSerializer): class PaiementSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Paiement` objects. """Serialize `cotisations.models.Paiement` objects.
""" """
class Meta: class Meta:
model = cotisations.Paiement model = cotisations.Paiement
fields = ('moyen', 'type_paiement', 'api_url') fields = ('moyen', 'api_url')
class CotisationSerializer(NamespacedHMSerializer): class CotisationSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Cotisation` objects. """Serialize `cotisations.models.Cotisation` objects.
""" """
class Meta: class Meta:
model = cotisations.Cotisation model = cotisations.Cotisation
fields = ('vente', 'type_cotisation', 'date_start', 'date_end', fields = ('vente', 'type_cotisation', 'date_start', 'date_end',
@ -125,6 +138,7 @@ class CotisationSerializer(NamespacedHMSerializer):
class MachineSerializer(NamespacedHMSerializer): class MachineSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Machine` objects. """Serialize `machines.models.Machine` objects.
""" """
class Meta: class Meta:
model = machines.Machine model = machines.Machine
fields = ('user', 'name', 'active', 'api_url') fields = ('user', 'name', 'active', 'api_url')
@ -133,6 +147,7 @@ class MachineSerializer(NamespacedHMSerializer):
class MachineTypeSerializer(NamespacedHMSerializer): class MachineTypeSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.MachineType` objects. """Serialize `machines.models.MachineType` objects.
""" """
class Meta: class Meta:
model = machines.MachineType model = machines.MachineType
fields = ('type', 'ip_type', 'api_url') fields = ('type', 'ip_type', 'api_url')
@ -141,6 +156,7 @@ class MachineTypeSerializer(NamespacedHMSerializer):
class IpTypeSerializer(NamespacedHMSerializer): class IpTypeSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.IpType` objects. """Serialize `machines.models.IpType` objects.
""" """
class Meta: class Meta:
model = machines.IpType model = machines.IpType
fields = ('type', 'extension', 'need_infra', 'domaine_ip_start', fields = ('type', 'extension', 'need_infra', 'domaine_ip_start',
@ -151,14 +167,17 @@ class IpTypeSerializer(NamespacedHMSerializer):
class VlanSerializer(NamespacedHMSerializer): class VlanSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Vlan` objects. """Serialize `machines.models.Vlan` objects.
""" """
class Meta: class Meta:
model = machines.Vlan model = machines.Vlan
fields = ('vlan_id', 'name', 'comment', 'api_url') fields = ('vlan_id', 'name', 'comment', 'arp_protect', 'dhcp_snooping',
'dhcpv6_snooping', 'igmp', 'mld', 'api_url')
class NasSerializer(NamespacedHMSerializer): class NasSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Nas` objects. """Serialize `machines.models.Nas` objects.
""" """
class Meta: class Meta:
model = machines.Nas model = machines.Nas
fields = ('name', 'nas_type', 'machine_type', 'port_access_mode', fields = ('name', 'nas_type', 'machine_type', 'port_access_mode',
@ -168,6 +187,7 @@ class NasSerializer(NamespacedHMSerializer):
class SOASerializer(NamespacedHMSerializer): class SOASerializer(NamespacedHMSerializer):
"""Serialize `machines.models.SOA` objects. """Serialize `machines.models.SOA` objects.
""" """
class Meta: class Meta:
model = machines.SOA model = machines.SOA
fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl', fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl',
@ -177,6 +197,7 @@ class SOASerializer(NamespacedHMSerializer):
class ExtensionSerializer(NamespacedHMSerializer): class ExtensionSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Extension` objects. """Serialize `machines.models.Extension` objects.
""" """
class Meta: class Meta:
model = machines.Extension model = machines.Extension
fields = ('name', 'need_infra', 'origin', 'origin_v6', 'soa', fields = ('name', 'need_infra', 'origin', 'origin_v6', 'soa',
@ -186,6 +207,7 @@ class ExtensionSerializer(NamespacedHMSerializer):
class MxSerializer(NamespacedHMSerializer): class MxSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Mx` objects. """Serialize `machines.models.Mx` objects.
""" """
class Meta: class Meta:
model = machines.Mx model = machines.Mx
fields = ('zone', 'priority', 'name', 'api_url') fields = ('zone', 'priority', 'name', 'api_url')
@ -194,13 +216,16 @@ class MxSerializer(NamespacedHMSerializer):
class DNameSerializer(NamespacedHMSerializer): class DNameSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.DName` objects. """Serialize `machines.models.DName` objects.
""" """
class Meta: class Meta:
model = machines.DName model = machines.DName
fields = ('zone', 'alias', 'api_url') fields = ('zone', 'alias', 'api_url')
class NsSerializer(NamespacedHMSerializer): class NsSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ns` objects. """Serialize `machines.models.Ns` objects.
""" """
class Meta: class Meta:
model = machines.Ns model = machines.Ns
fields = ('zone', 'ns', 'api_url') fields = ('zone', 'ns', 'api_url')
@ -209,6 +234,7 @@ class NsSerializer(NamespacedHMSerializer):
class TxtSerializer(NamespacedHMSerializer): class TxtSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Txt` objects. """Serialize `machines.models.Txt` objects.
""" """
class Meta: class Meta:
model = machines.Txt model = machines.Txt
fields = ('zone', 'field1', 'field2', 'api_url') fields = ('zone', 'field1', 'field2', 'api_url')
@ -217,14 +243,17 @@ class TxtSerializer(NamespacedHMSerializer):
class SrvSerializer(NamespacedHMSerializer): class SrvSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Srv` objects. """Serialize `machines.models.Srv` objects.
""" """
class Meta: class Meta:
model = machines.Srv model = machines.Srv
fields = ('service', 'protocole', 'extension', 'ttl', 'priority', fields = ('service', 'protocole', 'extension', 'ttl', 'priority',
'weight', 'port', 'target', 'api_url') 'weight', 'port', 'target', 'api_url')
class SshFpSerializer(NamespacedHMSerializer): class SshFpSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.SSHFP` objects. """Serialize `machines.models.SSHFP` objects.
""" """
class Meta: class Meta:
model = machines.SshFp model = machines.SshFp
field = ('machine', 'pub_key_entry', 'algo', 'comment', 'api_url') field = ('machine', 'pub_key_entry', 'algo', 'comment', 'api_url')
@ -245,6 +274,7 @@ class InterfaceSerializer(NamespacedHMSerializer):
class Ipv6ListSerializer(NamespacedHMSerializer): class Ipv6ListSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ipv6List` objects. """Serialize `machines.models.Ipv6List` objects.
""" """
class Meta: class Meta:
model = machines.Ipv6List model = machines.Ipv6List
fields = ('ipv6', 'interface', 'slaac_ip', 'api_url') fields = ('ipv6', 'interface', 'slaac_ip', 'api_url')
@ -253,6 +283,7 @@ class Ipv6ListSerializer(NamespacedHMSerializer):
class DomainSerializer(NamespacedHMSerializer): class DomainSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Domain` objects. """Serialize `machines.models.Domain` objects.
""" """
class Meta: class Meta:
model = machines.Domain model = machines.Domain
fields = ('interface_parent', 'name', 'extension', 'cname', fields = ('interface_parent', 'name', 'extension', 'cname',
@ -262,6 +293,7 @@ class DomainSerializer(NamespacedHMSerializer):
class IpListSerializer(NamespacedHMSerializer): class IpListSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.IpList` objects. """Serialize `machines.models.IpList` objects.
""" """
class Meta: class Meta:
model = machines.IpList model = machines.IpList
fields = ('ipv4', 'ip_type', 'need_infra', 'api_url') fields = ('ipv4', 'ip_type', 'need_infra', 'api_url')
@ -270,6 +302,7 @@ class IpListSerializer(NamespacedHMSerializer):
class ServiceSerializer(NamespacedHMSerializer): class ServiceSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Service` objects. """Serialize `machines.models.Service` objects.
""" """
class Meta: class Meta:
model = machines.Service model = machines.Service
fields = ('service_type', 'min_time_regen', 'regular_time_regen', fields = ('service_type', 'min_time_regen', 'regular_time_regen',
@ -279,6 +312,7 @@ class ServiceSerializer(NamespacedHMSerializer):
class ServiceLinkSerializer(NamespacedHMSerializer): class ServiceLinkSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Service_link` objects. """Serialize `machines.models.Service_link` objects.
""" """
class Meta: class Meta:
model = machines.Service_link model = machines.Service_link
fields = ('service', 'server', 'last_regen', 'asked_regen', fields = ('service', 'server', 'last_regen', 'asked_regen',
@ -305,11 +339,22 @@ class OuverturePortListSerializer(NamespacedHMSerializer):
class OuverturePortSerializer(NamespacedHMSerializer): class OuverturePortSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.OuverturePort` objects. """Serialize `machines.models.OuverturePort` objects.
""" """
class Meta: class Meta:
model = machines.OuverturePort model = machines.OuverturePort
fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url') fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url')
class RoleSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.OuverturePort` objects.
"""
servers = InterfaceSerializer(read_only=True, many=True)
class Meta:
model = machines.Role
fields = ('role_type', 'servers', 'api_url')
# PREFERENCES # PREFERENCES
@ -317,17 +362,21 @@ class OptionalUserSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalUser` objects. """Serialize `preferences.models.OptionalUser` objects.
""" """
tel_mandatory = serializers.BooleanField(source='is_tel_mandatory') tel_mandatory = serializers.BooleanField(source='is_tel_mandatory')
shell_default = serializers.StringRelatedField()
class Meta: class Meta:
model = preferences.OptionalUser model = preferences.OptionalUser
fields = ('tel_mandatory', 'user_solde', 'solde_negatif', 'max_solde', fields = ('tel_mandatory', 'gpg_fingerprint',
'min_online_payment', 'gpg_fingerprint', 'all_can_create_club', 'self_adhesion', 'shell_default',
'all_can_create_club', 'self_adhesion', 'shell_default') 'self_change_shell', 'local_email_accounts_enabled', 'local_email_domain',
'max_email_address',
)
class OptionalMachineSerializer(NamespacedHMSerializer): class OptionalMachineSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalMachine` objects. """Serialize `preferences.models.OptionalMachine` objects.
""" """
class Meta: class Meta:
model = preferences.OptionalMachine model = preferences.OptionalMachine
fields = ('password_machine', 'max_lambdauser_interfaces', fields = ('password_machine', 'max_lambdauser_interfaces',
@ -338,27 +387,45 @@ class OptionalMachineSerializer(NamespacedHMSerializer):
class OptionalTopologieSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalTopologie` objects. """Serialize `preferences.models.OptionalTopologie` objects.
""" """
switchs_management_interface_ip = serializers.CharField()
class Meta: class Meta:
model = preferences.OptionalTopologie model = preferences.OptionalTopologie
fields = ('radius_general_policy', 'vlan_decision_ok', fields = ('switchs_ip_type', 'switchs_web_management',
'vlan_decision_nok') 'switchs_web_management_ssl', 'switchs_rest_management',
'switchs_management_utils', 'switchs_management_interface_ip',
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
class RadiusOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.RadiusOption` objects
"""
class Meta:
model = preferences.RadiusOption
fields = ('radius_general_policy', 'unknown_machine',
'unknown_machine_vlan', 'unknown_port',
'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan',
'non_member', 'non_member_vlan', 'banned', 'banned_vlan',
'vlan_decision_ok')
class GeneralOptionSerializer(NamespacedHMSerializer): class GeneralOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.GeneralOption` objects. """Serialize `preferences.models.GeneralOption` objects.
""" """
class Meta: class Meta:
model = preferences.GeneralOption model = preferences.GeneralOption
fields = ('general_message', 'search_display_page', fields = ('general_message_fr', 'general_message_en',
'pagination_number', 'pagination_large_number', 'search_display_page', 'pagination_number',
'req_expire_hrs', 'site_name', 'email_from', 'GTU_sum_up', 'pagination_large_number', 'req_expire_hrs',
'GTU') 'site_name', 'main_site_url', 'email_from',
'GTU_sum_up', 'GTU')
class HomeServiceSerializer(NamespacedHMSerializer): class HomeServiceSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.Service` objects. """Serialize `preferences.models.Service` objects.
""" """
class Meta: class Meta:
model = preferences.Service model = preferences.Service
fields = ('name', 'url', 'description', 'image', 'api_url') fields = ('name', 'url', 'description', 'image', 'api_url')
@ -370,16 +437,17 @@ class HomeServiceSerializer(NamespacedHMSerializer):
class AssoOptionSerializer(NamespacedHMSerializer): class AssoOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.AssoOption` objects. """Serialize `preferences.models.AssoOption` objects.
""" """
class Meta: class Meta:
model = preferences.AssoOption model = preferences.AssoOption
fields = ('name', 'siret', 'adresse1', 'adresse2', 'contact', fields = ('name', 'siret', 'adresse1', 'adresse2', 'contact',
'telephone', 'pseudo', 'utilisateur_asso', 'payment', 'telephone', 'pseudo', 'utilisateur_asso', 'description')
'payment_id', 'payment_pass', 'description')
class HomeOptionSerializer(NamespacedHMSerializer): class HomeOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.HomeOption` objects. """Serialize `preferences.models.HomeOption` objects.
""" """
class Meta: class Meta:
model = preferences.HomeOption model = preferences.HomeOption
fields = ('facebook_url', 'twitter_url', 'twitter_account_name') fields = ('facebook_url', 'twitter_url', 'twitter_account_name')
@ -388,18 +456,19 @@ class HomeOptionSerializer(NamespacedHMSerializer):
class MailMessageOptionSerializer(NamespacedHMSerializer): class MailMessageOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.MailMessageOption` objects. """Serialize `preferences.models.MailMessageOption` objects.
""" """
class Meta: class Meta:
model = preferences.MailMessageOption model = preferences.MailMessageOption
fields = ('welcome_mail_fr', 'welcome_mail_en') fields = ('welcome_mail_fr', 'welcome_mail_en')
# TOPOLOGIE # TOPOLOGIE
class StackSerializer(NamespacedHMSerializer): class StackSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Stack` objects """Serialize `topologie.models.Stack` objects
""" """
class Meta: class Meta:
model = topologie.Stack model = topologie.Stack
fields = ('name', 'stack_id', 'details', 'member_id_min', fields = ('name', 'stack_id', 'details', 'member_id_min',
@ -409,6 +478,7 @@ class StackSerializer(NamespacedHMSerializer):
class AccessPointSerializer(NamespacedHMSerializer): class AccessPointSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.AccessPoint` objects """Serialize `topologie.models.AccessPoint` objects
""" """
class Meta: class Meta:
model = topologie.AccessPoint model = topologie.AccessPoint
fields = ('user', 'name', 'active', 'location', 'api_url') fields = ('user', 'name', 'active', 'location', 'api_url')
@ -418,6 +488,7 @@ class SwitchSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Switch` objects """Serialize `topologie.models.Switch` objects
""" """
port_amount = serializers.IntegerField(source='number') port_amount = serializers.IntegerField(source='number')
class Meta: class Meta:
model = topologie.Switch model = topologie.Switch
fields = ('user', 'name', 'active', 'port_amount', 'stack', fields = ('user', 'name', 'active', 'port_amount', 'stack',
@ -427,6 +498,7 @@ class SwitchSerializer(NamespacedHMSerializer):
class ServerSerializer(NamespacedHMSerializer): class ServerSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Server` objects """Serialize `topologie.models.Server` objects
""" """
class Meta: class Meta:
model = topologie.Server model = topologie.Server
fields = ('user', 'name', 'active', 'api_url') fields = ('user', 'name', 'active', 'api_url')
@ -435,6 +507,7 @@ class ServerSerializer(NamespacedHMSerializer):
class ModelSwitchSerializer(NamespacedHMSerializer): class ModelSwitchSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.ModelSwitch` objects """Serialize `topologie.models.ModelSwitch` objects
""" """
class Meta: class Meta:
model = topologie.ModelSwitch model = topologie.ModelSwitch
fields = ('reference', 'constructor', 'api_url') fields = ('reference', 'constructor', 'api_url')
@ -443,6 +516,7 @@ class ModelSwitchSerializer(NamespacedHMSerializer):
class ConstructorSwitchSerializer(NamespacedHMSerializer): class ConstructorSwitchSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.ConstructorSwitch` objects """Serialize `topologie.models.ConstructorSwitch` objects
""" """
class Meta: class Meta:
model = topologie.ConstructorSwitch model = topologie.ConstructorSwitch
fields = ('name', 'api_url') fields = ('name', 'api_url')
@ -451,6 +525,7 @@ class ConstructorSwitchSerializer(NamespacedHMSerializer):
class SwitchBaySerializer(NamespacedHMSerializer): class SwitchBaySerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.SwitchBay` objects """Serialize `topologie.models.SwitchBay` objects
""" """
class Meta: class Meta:
model = topologie.SwitchBay model = topologie.SwitchBay
fields = ('name', 'building', 'info', 'api_url') fields = ('name', 'building', 'info', 'api_url')
@ -459,6 +534,7 @@ class SwitchBaySerializer(NamespacedHMSerializer):
class BuildingSerializer(NamespacedHMSerializer): class BuildingSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Building` objects """Serialize `topologie.models.Building` objects
""" """
class Meta: class Meta:
model = topologie.Building model = topologie.Building
fields = ('name', 'api_url') fields = ('name', 'api_url')
@ -467,19 +543,34 @@ class BuildingSerializer(NamespacedHMSerializer):
class SwitchPortSerializer(NamespacedHMSerializer): class SwitchPortSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Port` objects """Serialize `topologie.models.Port` objects
""" """
get_port_profile = NamespacedHIField(view_name='portprofile-detail', read_only=True)
class Meta: class Meta:
model = topologie.Port model = topologie.Port
fields = ('switch', 'port', 'room', 'machine_interface', 'related', fields = ('switch', 'port', 'room', 'machine_interface', 'related',
'custom_profile', 'state', 'details', 'api_url') 'custom_profile', 'state', 'get_port_profile', 'details', 'api_url')
extra_kwargs = { extra_kwargs = {
'related': {'view_name': 'switchport-detail'}, 'related': {'view_name': 'switchport-detail'},
'api_url': {'view_name': 'switchport-detail'}, 'api_url': {'view_name': 'switchport-detail'},
} }
class PortProfileSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Room` objects
"""
class Meta:
model = topologie.PortProfile
fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged',
'radius_type', 'radius_mode', 'speed', 'mac_limit', 'flow_control',
'dhcp_snooping', 'dhcpv6_snooping', 'dhcpv6_snooping', 'arp_protect',
'ra_guard', 'loop_protect', 'api_url')
class RoomSerializer(NamespacedHMSerializer): class RoomSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Room` objects """Serialize `topologie.models.Room` objects
""" """
class Meta: class Meta:
model = topologie.Room model = topologie.Room
fields = ('name', 'details', 'api_url') fields = ('name', 'details', 'api_url')
@ -552,9 +643,9 @@ class AdherentSerializer(NamespacedHMSerializer):
'shell': {'view_name': 'shell-detail'} 'shell': {'view_name': 'shell-detail'}
} }
class HomeCreationSerializer(NamespacedHMSerializer):
"""Serialize 'users.models.User' minimal infos to create home class BasicUserSerializer(NamespacedHMSerializer):
""" """Serialize 'users.models.User' minimal infos"""
uid = serializers.IntegerField(source='uid_number') uid = serializers.IntegerField(source='uid_number')
gid = serializers.IntegerField(source='gid_number') gid = serializers.IntegerField(source='gid_number')
@ -562,9 +653,11 @@ class HomeCreationSerializer(NamespacedHMSerializer):
model = users.User model = users.User
fields = ('pseudo', 'uid', 'gid') fields = ('pseudo', 'uid', 'gid')
class ServiceUserSerializer(NamespacedHMSerializer): class ServiceUserSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ServiceUser` objects. """Serialize `users.models.ServiceUser` objects.
""" """
class Meta: class Meta:
model = users.ServiceUser model = users.ServiceUser
fields = ('pseudo', 'access_group', 'comment', 'api_url') fields = ('pseudo', 'access_group', 'comment', 'api_url')
@ -573,6 +666,7 @@ class ServiceUserSerializer(NamespacedHMSerializer):
class SchoolSerializer(NamespacedHMSerializer): class SchoolSerializer(NamespacedHMSerializer):
"""Serialize `users.models.School` objects. """Serialize `users.models.School` objects.
""" """
class Meta: class Meta:
model = users.School model = users.School
fields = ('name', 'api_url') fields = ('name', 'api_url')
@ -581,6 +675,7 @@ class SchoolSerializer(NamespacedHMSerializer):
class ListRightSerializer(NamespacedHMSerializer): class ListRightSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ListRight` objects. """Serialize `users.models.ListRight` objects.
""" """
class Meta: class Meta:
model = users.ListRight model = users.ListRight
fields = ('unix_name', 'gid', 'critical', 'details', 'api_url') fields = ('unix_name', 'gid', 'critical', 'details', 'api_url')
@ -589,6 +684,7 @@ class ListRightSerializer(NamespacedHMSerializer):
class ShellSerializer(NamespacedHMSerializer): class ShellSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ListShell` objects. """Serialize `users.models.ListShell` objects.
""" """
class Meta: class Meta:
model = users.ListShell model = users.ListShell
fields = ('shell', 'api_url') fields = ('shell', 'api_url')
@ -622,6 +718,7 @@ class EMailAddressSerializer(NamespacedHMSerializer):
"""Serialize `users.models.EMailAddress` objects. """Serialize `users.models.EMailAddress` objects.
""" """
user = serializers.CharField(source='user.pseudo', read_only=True) user = serializers.CharField(source='user.pseudo', read_only=True)
class Meta: class Meta:
model = users.EMailAddress model = users.EMailAddress
fields = ('user', 'local_part', 'complete_email_address', 'api_url') fields = ('user', 'local_part', 'complete_email_address', 'api_url')
@ -644,6 +741,90 @@ class ServiceRegenSerializer(NamespacedHMSerializer):
'api_url': {'view_name': 'serviceregen-detail'} 'api_url': {'view_name': 'serviceregen-detail'}
} }
# Switches et ports
class InterfaceVlanSerializer(NamespacedHMSerializer):
domain = serializers.CharField(read_only=True)
ipv4 = serializers.CharField(read_only=True)
ipv6 = Ipv6ListSerializer(read_only=True, many=True)
vlan_id = serializers.IntegerField(source='type.ip_type.vlan.vlan_id', read_only=True)
class Meta:
model = machines.Interface
fields = ('ipv4', 'ipv6', 'domain', 'vlan_id')
class InterfaceRoleSerializer(NamespacedHMSerializer):
interface = InterfaceVlanSerializer(source='machine.interface_set', read_only=True, many=True)
class Meta:
model = machines.Interface
fields = ('interface',)
class RoleSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.OuverturePort` objects.
"""
servers = InterfaceRoleSerializer(read_only=True, many=True)
class Meta:
model = machines.Role
fields = ('role_type', 'servers', 'specific_role')
class VlanPortSerializer(NamespacedHMSerializer):
class Meta:
model = machines.Vlan
fields = ('vlan_id', 'name')
class ProfilSerializer(NamespacedHMSerializer):
vlan_untagged = VlanSerializer(read_only=True)
vlan_tagged = VlanPortSerializer(read_only=True, many=True)
class Meta:
model = topologie.PortProfile
fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged', 'radius_type', 'radius_mode', 'speed', 'mac_limit', 'flow_control', 'dhcp_snooping', 'dhcpv6_snooping', 'arp_protect', 'ra_guard', 'loop_protect', 'vlan_untagged', 'vlan_tagged')
class ModelSwitchSerializer(NamespacedHMSerializer):
constructor = serializers.CharField(read_only=True)
class Meta:
model = topologie.ModelSwitch
fields = ('reference', 'firmware', 'constructor')
class SwitchBaySerializer(NamespacedHMSerializer):
class Meta:
model = topologie.SwitchBay
fields = ('name',)
class PortsSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ipv6List` objects.
"""
get_port_profile = ProfilSerializer(read_only=True)
class Meta:
model = topologie.Port
fields = ('state', 'port', 'pretty_name', 'get_port_profile')
class SwitchPortSerializer(serializers.ModelSerializer):
"""Serialize the data about the switches"""
ports = PortsSerializer(many=True, read_only=True)
model = ModelSwitchSerializer(read_only=True)
switchbay = SwitchBaySerializer(read_only=True)
class Meta:
model = topologie.Switch
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value',
'list_modules')
# LOCAL EMAILS # LOCAL EMAILS
@ -667,6 +848,7 @@ class FirewallPortListSerializer(serializers.ModelSerializer):
model = machines.OuverturePort model = machines.OuverturePort
fields = ('begin', 'end', 'protocole', 'io', 'show_port') fields = ('begin', 'end', 'protocole', 'io', 'show_port')
class FirewallOuverturePortListSerializer(serializers.ModelSerializer): class FirewallOuverturePortListSerializer(serializers.ModelSerializer):
tcp_ports_in = FirewallPortListSerializer(many=True, read_only=True) tcp_ports_in = FirewallPortListSerializer(many=True, read_only=True)
udp_ports_in = FirewallPortListSerializer(many=True, read_only=True) udp_ports_in = FirewallPortListSerializer(many=True, read_only=True)
@ -677,6 +859,7 @@ class FirewallOuverturePortListSerializer(serializers.ModelSerializer):
model = machines.OuverturePortList model = machines.OuverturePortList
fields = ('tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', 'udp_ports_out') fields = ('tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', 'udp_ports_out')
class SubnetPortsOpenSerializer(serializers.ModelSerializer): class SubnetPortsOpenSerializer(serializers.ModelSerializer):
ouverture_ports = FirewallOuverturePortListSerializer(read_only=True) ouverture_ports = FirewallOuverturePortListSerializer(read_only=True)
@ -684,6 +867,7 @@ class SubnetPortsOpenSerializer(serializers.ModelSerializer):
model = machines.IpType model = machines.IpType
fields = ('type', 'domaine_ip_start', 'domaine_ip_stop', 'complete_prefixv6', 'ouverture_ports') fields = ('type', 'domaine_ip_start', 'domaine_ip_stop', 'complete_prefixv6', 'ouverture_ports')
class InterfacePortsOpenSerializer(serializers.ModelSerializer): class InterfacePortsOpenSerializer(serializers.ModelSerializer):
port_lists = FirewallOuverturePortListSerializer(read_only=True, many=True) port_lists = FirewallOuverturePortListSerializer(read_only=True, many=True)
ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True) ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True)
@ -693,6 +877,7 @@ class InterfacePortsOpenSerializer(serializers.ModelSerializer):
model = machines.Interface model = machines.Interface
fields = ('port_lists', 'ipv4', 'ipv6') fields = ('port_lists', 'ipv4', 'ipv6')
# DHCP # DHCP
@ -717,6 +902,7 @@ class SOARecordSerializer(SOASerializer):
"""Serialize `machines.models.SOA` objects with the data needed to """Serialize `machines.models.SOA` objects with the data needed to
generate a SOA DNS record. generate a SOA DNS record.
""" """
class Meta: class Meta:
model = machines.SOA model = machines.SOA
fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl') fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl')
@ -726,6 +912,7 @@ class OriginV4RecordSerializer(IpListSerializer):
"""Serialize `machines.models.IpList` objects with the data needed to """Serialize `machines.models.IpList` objects with the data needed to
generate an IPv4 Origin DNS record. generate an IPv4 Origin DNS record.
""" """
class Meta(IpListSerializer.Meta): class Meta(IpListSerializer.Meta):
fields = ('ipv4',) fields = ('ipv4',)
@ -754,6 +941,7 @@ class TXTRecordSerializer(TxtSerializer):
"""Serialize `machines.models.Txt` objects with the data needed to """Serialize `machines.models.Txt` objects with the data needed to
generate a TXT DNS record. generate a TXT DNS record.
""" """
class Meta(TxtSerializer.Meta): class Meta(TxtSerializer.Meta):
fields = ('field1', 'field2') fields = ('field1', 'field2')
@ -772,6 +960,7 @@ class SSHFPRecordSerializer(SshFpSerializer):
"""Serialize `machines.models.SshFp` objects with the data needed to """Serialize `machines.models.SshFp` objects with the data needed to
generate a SSHFP DNS record. generate a SSHFP DNS record.
""" """
class Meta(SshFpSerializer.Meta): class Meta(SshFpSerializer.Meta):
fields = ('algo_id', 'hash') fields = ('algo_id', 'hash')
@ -823,6 +1012,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer):
model = machines.Domain model = machines.Domain
fields = ('alias', 'hostname') fields = ('alias', 'hostname')
class DNAMERecordSerializer(serializers.ModelSerializer):
"""Serialize `machines.models.Domain` objects with the data needed to
generate a DNAME DNS record.
"""
alias = serializers.CharField(read_only=True)
zone = serializers.CharField(read_only=True)
class Meta:
model = machines.DName
fields = ('alias', 'zone')
class DNSZonesSerializer(serializers.ModelSerializer): class DNSZonesSerializer(serializers.ModelSerializer):
"""Serialize the data about DNS Zones. """Serialize the data about DNS Zones.
@ -837,13 +1037,33 @@ class DNSZonesSerializer(serializers.ModelSerializer):
a_records = ARecordSerializer(many=True, source='get_associated_a_records') a_records = ARecordSerializer(many=True, source='get_associated_a_records')
aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records') aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records')
cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records') cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records')
dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records')
sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records') sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records')
class Meta: class Meta:
model = machines.Extension model = machines.Extension
fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6', fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6',
'mx_records', 'txt_records', 'srv_records', 'a_records', 'mx_records', 'txt_records', 'srv_records', 'a_records',
'aaaa_records', 'cname_records', 'sshfp_records') 'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records')
#REMINDER
class ReminderUsersSerializer(UserSerializer):
"""Serialize the data about a mailing member.
"""
class Meta(UserSerializer.Meta):
fields = ('get_full_name', 'get_mail')
class ReminderSerializer(serializers.ModelSerializer):
"""
Serialize the data about a reminder
"""
users_to_remind = ReminderUsersSerializer(many=True)
class Meta:
model = preferences.Reminder
fields = ('days','message','users_to_remind')
class DNSReverseZonesSerializer(serializers.ModelSerializer): class DNSReverseZonesSerializer(serializers.ModelSerializer):
@ -858,22 +1078,24 @@ class DNSReverseZonesSerializer(serializers.ModelSerializer):
ptr_records = ARecordSerializer(many=True, source='get_associated_ptr_records') ptr_records = ARecordSerializer(many=True, source='get_associated_ptr_records')
ptr_v6_records = AAAARecordSerializer(many=True, source='get_associated_ptr_v6_records') ptr_v6_records = AAAARecordSerializer(many=True, source='get_associated_ptr_v6_records')
class Meta: class Meta:
model = machines.IpType model = machines.IpType
fields = ('type', 'extension', 'soa', 'ns_records', 'mx_records', fields = ('type', 'extension', 'soa', 'ns_records', 'mx_records',
'txt_records', 'ptr_records', 'ptr_v6_records', 'cidrs', 'txt_records', 'ptr_records', 'ptr_v6_records', 'cidrs',
'prefix_v6', 'prefix_v6_length') 'prefix_v6', 'prefix_v6_length')
# MAILING # MAILING
class MailingMemberSerializer(UserSerializer): class MailingMemberSerializer(UserSerializer):
"""Serialize the data about a mailing member. """Serialize the data about a mailing member.
""" """
class Meta(UserSerializer.Meta): class Meta(UserSerializer.Meta):
fields = ('name', 'pseudo', 'get_mail') fields = ('name', 'pseudo', 'get_mail')
class MailingSerializer(ClubSerializer): class MailingSerializer(ClubSerializer):
"""Serialize the data about a mailing. """Serialize the data about a mailing.
""" """

View file

@ -21,10 +21,11 @@
"""Defines the test suite for the API """Defines the test suite for the API
""" """
import json
import datetime import datetime
from rest_framework.test import APITestCase import json
from requests import codes from requests import codes
from rest_framework.test import APITestCase
import cotisations.models as cotisations import cotisations.models as cotisations
import machines.models as machines import machines.models as machines
@ -676,6 +677,7 @@ class APIEndpointsTestCase(APITestCase):
formats=[None, 'json', 'api'], formats=[None, 'json', 'api'],
assert_more=assert_more) assert_more=assert_more)
class APIPaginationTestCase(APITestCase): class APIPaginationTestCase(APITestCase):
"""Test case to check that the pagination is used on all endpoints that """Test case to check that the pagination is used on all endpoints that
should use it. should use it.
@ -756,7 +758,7 @@ class APIPaginationTestCase(APITestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.superuser.delete() cls.superuser.delete()
super().tearDownClass() super(APIPaginationTestCase, self).tearDownClass()
def test_pagination(self): def test_pagination(self):
"""Tests that every endpoint is using the pagination correctly. """Tests that every endpoint is using the pagination correctly.
@ -776,4 +778,3 @@ class APIPaginationTestCase(APITestCase):
assert 'previous' in res_json.keys() assert 'previous' in res_json.keys()
assert 'results' in res_json.keys() assert 'results' in res_json.keys()
assert not len('results') > 100 assert not len('results') > 100

View file

@ -32,7 +32,6 @@ from django.conf.urls import url, include
from . import views from . import views
from .routers import AllViewsRouter from .routers import AllViewsRouter
router = AllViewsRouter() router = AllViewsRouter()
# COTISATIONS # COTISATIONS
router.register_viewset(r'cotisations/facture', views.FactureViewSet) router.register_viewset(r'cotisations/facture', views.FactureViewSet)
@ -63,10 +62,12 @@ router.register_viewset(r'machines/service', views.ServiceViewSet)
router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink') router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink')
router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet) router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet)
router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet) router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet)
router.register_viewset(r'machines/role', views.RoleViewSet)
# PREFERENCES # PREFERENCES
router.register_view(r'preferences/optionaluser', views.OptionalUserView), router.register_view(r'preferences/optionaluser', views.OptionalUserView),
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView), router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView),
router.register_view(r'preferences/radiusoption', views.RadiusOptionView),
router.register_view(r'preferences/generaloption', views.GeneralOptionView), router.register_view(r'preferences/generaloption', views.GeneralOptionView),
router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'), router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
router.register_view(r'preferences/assooption', views.AssoOptionView), router.register_view(r'preferences/assooption', views.AssoOptionView),
@ -81,12 +82,15 @@ router.register_viewset(r'topologie/modelswitch', views.ModelSwitchViewSet)
router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet) router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet)
router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet) router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet)
router.register_viewset(r'topologie/building', views.BuildingViewSet) router.register_viewset(r'topologie/building', views.BuildingViewSet)
router.register(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport')
router.register_viewset(r'topologie/portprofile', views.PortProfileViewSet, base_name='portprofile')
router.register_viewset(r'topologie/room', views.RoomViewSet) router.register_viewset(r'topologie/room', views.RoomViewSet)
router.register(r'topologie/portprofile', views.PortProfileViewSet) router.register(r'topologie/portprofile', views.PortProfileViewSet)
# USERS # USERS
router.register_viewset(r'users/user', views.UserViewSet) router.register_viewset(r'users/user', views.UserViewSet, base_name='user')
router.register_viewset(r'users/homecreation', views.HomeCreationViewSet) router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation')
router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser')
router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser')
router.register_viewset(r'users/club', views.ClubViewSet) router.register_viewset(r'users/club', views.ClubViewSet)
router.register_viewset(r'users/adherent', views.AdherentViewSet) router.register_viewset(r'users/adherent', views.AdherentViewSet)
router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet)
@ -105,6 +109,11 @@ router.register_view(r'localemail/users', views.LocalEmailUsersView),
# Firewall # Firewall
router.register_view(r'firewall/subnet-ports', views.SubnetPortsOpenView), router.register_view(r'firewall/subnet-ports', views.SubnetPortsOpenView),
router.register_view(r'firewall/interface-ports', views.InterfacePortsOpenView), router.register_view(r'firewall/interface-ports', views.InterfacePortsOpenView),
# Switches config
router.register_view(r'switchs/ports-config', views.SwitchPortView),
router.register_view(r'switchs/role', views.RoleView),
# Reminder
router.register_view(r'reminder/get-users', views.ReminderView),
# DNS # DNS
router.register_view(r'dns/zones', views.DNSZonesView), router.register_view(r'dns/zones', views.DNSZonesView),
router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView), router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView),
@ -114,7 +123,6 @@ router.register_view(r'mailing/club', views.ClubMailingView),
# TOKEN AUTHENTICATION # TOKEN AUTHENTICATION
router.register_view(r'token-auth', views.ObtainExpiringAuthToken) router.register_view(r'token-auth', views.ObtainExpiringAuthToken)
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
] ]

View file

@ -29,10 +29,11 @@ the response (JSON or other), the CSRF exempting, ...
import datetime import datetime
from django.conf import settings from django.conf import settings
from rest_framework.authtoken.views import ObtainAuthToken from django.db.models import Q
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework import viewsets, generics, views from rest_framework import viewsets, generics, views
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
import cotisations.models as cotisations import cotisations.models as cotisations
import machines.models as machines import machines.models as machines
@ -40,7 +41,6 @@ import preferences.models as preferences
import topologie.models as topologie import topologie.models as topologie
import users.models as users import users.models as users
from re2o.utils import all_active_interfaces, all_has_access from re2o.utils import all_active_interfaces, all_has_access
from . import serializers from . import serializers
from .pagination import PageSizedPagination from .pagination import PageSizedPagination
from .permissions import ACLPermission from .permissions import ACLPermission
@ -55,6 +55,12 @@ class FactureViewSet(viewsets.ReadOnlyModelViewSet):
queryset = cotisations.Facture.objects.all() queryset = cotisations.Facture.objects.all()
serializer_class = serializers.FactureSerializer serializer_class = serializers.FactureSerializer
class FactureViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `cotisations.models.Facture` objects.
"""
queryset = cotisations.BaseInvoice.objects.all()
serializer_class = serializers.BaseInvoiceSerializer
class VenteViewSet(viewsets.ReadOnlyModelViewSet): class VenteViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `cotisations.models.Vente` objects. """Exposes list and details of `cotisations.models.Vente` objects.
@ -163,6 +169,7 @@ 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): class DNameViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.DName` objects. """Exposes list and details of `machines.models.DName` objects.
""" """
@ -241,6 +248,13 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.OuverturePortSerializer serializer_class = serializers.OuverturePortSerializer
class RoleViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.Machine` objects.
"""
queryset = machines.Role.objects.all()
serializer_class = serializers.RoleSerializer
# PREFERENCES # PREFERENCES
# Those views differ a bit because there is only one object # Those views differ a bit because there is only one object
# to display, so we don't bother with the listing part # to display, so we don't bother with the listing part
@ -278,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView):
return preferences.OptionalTopologie.objects.first() return preferences.OptionalTopologie.objects.first()
class RadiusOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.OptionalTopologie` settings.
"""
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.RadiusOption.can_view_all]}
serializer_class = serializers.RadiusOptionSerializer
def get_object(self):
return preferences.RadiusOption.objects.first()
class GeneralOptionView(generics.RetrieveAPIView): class GeneralOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.GeneralOption` settings. """Exposes details of `preferences.models.GeneralOption` settings.
""" """
@ -396,6 +421,13 @@ class SwitchPortViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.SwitchPortSerializer serializer_class = serializers.SwitchPortSerializer
class PortProfileViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.PortProfile` objects.
"""
queryset = topologie.PortProfile.objects.all()
serializer_class = serializers.PortProfileSerializer
class RoomViewSet(viewsets.ReadOnlyModelViewSet): class RoomViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.Room` objects. """Exposes list and details of `topologie.models.Room` objects.
""" """
@ -409,6 +441,7 @@ class PortProfileViewSet(viewsets.ReadOnlyModelViewSet):
queryset = topologie.PortProfile.objects.all() queryset = topologie.PortProfile.objects.all()
serializer_class = serializers.PortProfileSerializer serializer_class = serializers.PortProfileSerializer
# USER # USER
@ -418,11 +451,25 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = users.User.objects.all() queryset = users.User.objects.all()
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet): class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes. """Exposes infos of `users.models.Users` objects to create homes.
""" """
queryset = users.User.objects.all() queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
serializer_class = serializers.HomeCreationSerializer serializer_class = serializers.BasicUserSerializer
class NormalUserViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users`without specific rights objects."""
queryset = users.User.objects.exclude(groups__listright__critical=True).distinct()
serializer_class = serializers.BasicUserSerializer
class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users`without specific rights objects."""
queryset = users.User.objects.filter(groups__listright__critical=True).distinct()
serializer_class = serializers.BasicUserSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet): class ClubViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.Club` objects. """Exposes list and details of `users.models.Club` objects.
@ -514,6 +561,31 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
queryset = queryset.filter(server__domain__name__iexact=hostname) queryset = queryset.filter(server__domain__name__iexact=hostname)
return queryset return queryset
# Config des switches
class SwitchPortView(generics.ListAPIView):
"""Output each port of a switch, to be serialized with
additionnal informations (profiles etc)
"""
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
serializer_class = serializers.SwitchPortSerializer
# Rappel fin adhésion
class ReminderView(generics.ListAPIView):
"""Output for users to remind an end of their subscription.
"""
queryset = preferences.Reminder.objects.all()
serializer_class = serializers.ReminderSerializer
class RoleView(generics.ListAPIView):
"""Output of roles for each server
"""
queryset = machines.Role.objects.all().prefetch_related('servers')
serializer_class = serializers.RoleSerializer
# LOCAL EMAILS # LOCAL EMAILS
@ -539,9 +611,11 @@ class HostMacIpView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in """Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files. order to build the DHCP lease files.
""" """
queryset = all_active_interfaces()
serializer_class = serializers.HostMacIpSerializer serializer_class = serializers.HostMacIpSerializer
def get_queryset(self):
return all_active_interfaces()
# Firewall # Firewall
@ -549,10 +623,12 @@ class SubnetPortsOpenView(generics.ListAPIView):
queryset = machines.IpType.objects.all() queryset = machines.IpType.objects.all()
serializer_class = serializers.SubnetPortsOpenSerializer serializer_class = serializers.SubnetPortsOpenSerializer
class InterfacePortsOpenView(generics.ListAPIView): class InterfacePortsOpenView(generics.ListAPIView):
queryset = machines.Interface.objects.filter(port_lists__isnull=False).distinct() queryset = machines.Interface.objects.filter(port_lists__isnull=False).distinct()
serializer_class = serializers.InterfacePortsOpenSerializer serializer_class = serializers.InterfacePortsOpenSerializer
# DNS # DNS
@ -570,6 +646,7 @@ class DNSZonesView(generics.ListAPIView):
.all()) .all())
serializer_class = serializers.DNSZonesSerializer serializer_class = serializers.DNSZonesSerializer
class DNSReverseZonesView(generics.ListAPIView): class DNSReverseZonesView(generics.ListAPIView):
"""Exposes the detailed information about each extension (hostnames, """Exposes the detailed information about each extension (hostnames,
IPs, DNS records, etc.) in order to build the DNS zone files. IPs, DNS records, etc.) in order to build the DNS zone files.
@ -578,8 +655,6 @@ class DNSReverseZonesView(generics.ListAPIView):
serializer_class = serializers.DNSReverseZonesSerializer serializer_class = serializers.DNSReverseZonesSerializer
# MAILING # MAILING
@ -617,6 +692,7 @@ class ObtainExpiringAuthToken(ObtainAuthToken):
`rest_framework.auth_token.views.ObtainAuthToken` view except that the `rest_framework.auth_token.views.ObtainAuthToken` view except that the
expiration time is send along with the token as an addtional information. expiration time is send along with the token as an addtional information.
""" """
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)

View file

@ -14,4 +14,7 @@ libjs-jquery
libjs-jquery-ui libjs-jquery-ui
libjs-jquery-timepicker libjs-jquery-timepicker
libjs-bootstrap libjs-bootstrap
fonts-font-awesome
graphviz graphviz
git
gettext

View file

@ -30,7 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice from .models import CustomInvoice, CostEstimate
class FactureAdmin(VersionAdmin): class FactureAdmin(VersionAdmin):
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass pass
class CostEstimateAdmin(VersionAdmin):
"""Admin class for cost estimates."""
pass
class CustomInvoiceAdmin(VersionAdmin): class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices.""" """Admin class for custom invoices."""
pass pass
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin) admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin) admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(CustomInvoice, CustomInvoiceAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin)
admin.site.register(CostEstimate, CostEstimateAdmin)

View file

@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
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, CustomInvoice from .models import (
Article, Paiement, Facture, Banque,
CustomInvoice, Vente, CostEstimate,
)
from .payment_methods import balance from .payment_methods import balance
@ -102,9 +105,46 @@ class SelectArticleForm(FormRevMixin, Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
user = kwargs.pop('user') user = kwargs.pop('user')
target_user = kwargs.pop('target_user') target_user = kwargs.pop('target_user', None)
super(SelectArticleForm, self).__init__(*args, **kwargs) super(SelectArticleForm, self).__init__(*args, **kwargs)
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user) self.fields['article'].queryset = Article.find_allowed_articles(
user, target_user)
class DiscountForm(Form):
"""
Form used in oder to create a discount on an invoice.
"""
is_relative = forms.BooleanField(
label=_("Discount is on percentage."),
required=False,
)
discount = forms.DecimalField(
label=_("Discount"),
max_value=100,
min_value=0,
max_digits=5,
decimal_places=2,
required=False,
)
def apply_to_invoice(self, invoice):
invoice_price = invoice.prix_total()
discount = self.cleaned_data['discount']
is_relative = self.cleaned_data['is_relative']
if is_relative:
amount = discount/100 * invoice_price
else:
amount = discount
if amount:
name = _("{}% discount") if is_relative else _("{}€ discount")
name = name.format(discount)
Vente.objects.create(
facture=invoice,
name=name,
prix=-amount,
number=1
)
class CustomInvoiceForm(FormRevMixin, ModelForm): class CustomInvoiceForm(FormRevMixin, ModelForm):
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
fields = '__all__' fields = '__all__'
class CostEstimateForm(FormRevMixin, ModelForm):
"""
Form used to create a cost estimate.
"""
class Meta:
model = CostEstimate
exclude = ['paid', 'final_invoice']
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):
""" """
Form used to create an article. Form used to create an article.
@ -233,7 +282,7 @@ class RechargeForm(FormRevMixin, Form):
""" """
Form used to refill a user's balance Form used to refill a user's balance
""" """
value = forms.FloatField( value = forms.DecimalField(
label=_("Amount"), label=_("Amount"),
min_value=0.01, min_value=0.01,
validators=[] validators=[]
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
super(RechargeForm, self).__init__(*args, **kwargs) super(RechargeForm, self).__init__(*args, **kwargs)
self.fields['payment'].empty_label = \ self.fields['payment'].empty_label = \
_("Select a payment method") _("Select a payment method")
self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True) self.fields['payment'].queryset = Paiement.find_allowed_payments(
user_source).exclude(is_balance=True)
def clean(self): def clean(self):
""" """
@ -260,10 +310,9 @@ class RechargeForm(FormRevMixin, Form):
if balance_method.maximum_balance is not None and \ if balance_method.maximum_balance is not None and \
value + self.user.solde > balance_method.maximum_balance: value + self.user.solde > balance_method.maximum_balance:
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': balance_method.maximum_balance 'max_online_balance': balance_method.maximum_balance
} }
) )
return self.cleaned_data return self.cleaned_data

View file

@ -21,7 +21,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2.5\n" "Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-18 13:17+0200\n" "POT-Creation-Date: 2019-01-12 16:50+0100\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n" "PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n" "Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@ -33,79 +33,98 @@ msgstr ""
msgid "You don't have the right to view this application." msgid "You don't have the right to view this application."
msgstr "Vous n'avez pas le droit de voir cette application." msgstr "Vous n'avez pas le droit de voir cette application."
#: forms.py:63 forms.py:274 #: forms.py:66 forms.py:299
msgid "Select a payment method" msgid "Select a payment method"
msgstr "Sélectionnez un moyen de paiement" msgstr "Sélectionnez un moyen de paiement"
#: forms.py:66 models.py:510 #: forms.py:69 models.py:579
msgid "Member" msgid "Member"
msgstr "Adhérent" msgstr "Adhérent"
#: forms.py:68 #: forms.py:71
msgid "Select the proprietary member" msgid "Select the proprietary member"
msgstr "Sélectionnez l'adhérent propriétaire" msgstr "Sélectionnez l'adhérent propriétaire"
#: forms.py:69 #: forms.py:72
msgid "Validated invoice" msgid "Validated invoice"
msgstr "Facture validée" msgstr "Facture validée"
#: forms.py:82 #: forms.py:85
msgid "A payment method must be specified." msgid "A payment method must be specified."
msgstr "Un moyen de paiement doit être renseigné." msgstr "Un moyen de paiement doit être renseigné."
#: forms.py:96 forms.py:120 templates/cotisations/aff_article.html:33 #: forms.py:97 templates/cotisations/aff_article.html:33
#: templates/cotisations/facture.html:61 #: templates/cotisations/facture.html:67
msgid "Article" msgid "Article"
msgstr "Article" msgstr "Article"
#: forms.py:100 forms.py:124 templates/cotisations/edit_facture.html:46 #: forms.py:101 templates/cotisations/edit_facture.html:50
msgid "Quantity" msgid "Quantity"
msgstr "Quantité" msgstr "Quantité"
#: forms.py:154 #: forms.py:119
msgid "Discount is on percentage."
msgstr "La réduction est en pourcentage."
#: forms.py:123 templates/cotisations/facture.html:78
msgid "Discount"
msgstr "Réduction"
#: forms.py:140
#, python-format
msgid "{}% discount"
msgstr "{}% de réduction"
#: forms.py:140
msgid "{}€ discount"
msgstr "{}€ de réduction"
#: forms.py:179
msgid "Article name" msgid "Article name"
msgstr "Nom de l'article" msgstr "Nom de l'article"
#: forms.py:164 templates/cotisations/sidebar.html:50 #: forms.py:189 templates/cotisations/sidebar.html:55
msgid "Available articles" msgid "Available articles"
msgstr "Articles disponibles" msgstr "Articles disponibles"
#: forms.py:192 #: forms.py:217
msgid "Payment method name" msgid "Payment method name"
msgstr "Nom du moyen de paiement" msgstr "Nom du moyen de paiement"
#: forms.py:204 #: forms.py:229
msgid "Available payment methods" msgid "Available payment methods"
msgstr "Moyens de paiement disponibles" msgstr "Moyens de paiement disponibles"
#: forms.py:230 #: forms.py:255
msgid "Bank name" msgid "Bank name"
msgstr "Nom de la banque" msgstr "Nom de la banque"
#: forms.py:242 #: forms.py:267
msgid "Available banks" msgid "Available banks"
msgstr "Banques disponibles" msgstr "Banques disponibles"
#: forms.py:261 #: forms.py:286
msgid "Amount" msgid "Amount"
msgstr "Montant" msgstr "Montant"
#: forms.py:267 templates/cotisations/aff_cotisations.html:44 #: forms.py:292 templates/cotisations/aff_cost_estimate.html:42
#: templates/cotisations/aff_cotisations.html:44
#: templates/cotisations/aff_custom_invoice.html:42 #: templates/cotisations/aff_custom_invoice.html:42
#: templates/cotisations/control.html:66 #: templates/cotisations/control.html:66
msgid "Payment method" msgid "Payment method"
msgstr "Moyen de paiement" msgstr "Moyen de paiement"
#: forms.py:287 #: forms.py:313
#, python-format #, python-format
msgid "" msgid ""
"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 €."
msgstr "" msgstr ""
"Le montant demandé trop grand. Votre solde ne peut excéder " "Le montant demandé est trop grand. Votre solde ne peut excéder "
"%(max_online_balance)s €." "%(max_online_balance)s €."
#: models.py:60 templates/cotisations/aff_cotisations.html:48 #: models.py:60 templates/cotisations/aff_cost_estimate.html:46
#: templates/cotisations/aff_cotisations.html:48
#: templates/cotisations/aff_custom_invoice.html:46 #: templates/cotisations/aff_custom_invoice.html:46
#: templates/cotisations/control.html:70 #: templates/cotisations/control.html:70
msgid "Date" msgid "Date"
@ -133,9 +152,9 @@ msgstr "Peut voir un objet facture"
#: models.py:158 #: models.py:158
msgid "Can edit all the previous invoices" msgid "Can edit all the previous invoices"
msgstr "Peut modifier toutes les factures existantes" msgstr "Peut modifier toutes les factures précédentes"
#: models.py:160 models.py:305 #: models.py:160 models.py:373
msgid "invoice" msgid "invoice"
msgstr "facture" msgstr "facture"
@ -156,128 +175,149 @@ msgid ""
"You don't have the right to edit an invoice already controlled or " "You don't have the right to edit an invoice already controlled or "
"invalidated." "invalidated."
msgstr "" msgstr ""
"Vous n'avez pas le droit de modifier une facture précedemment contrôlée ou " "Vous n'avez pas le droit de modifier une facture précédemment contrôlée ou "
"invalidée." "invalidée."
#: models.py:184 #: models.py:184
msgid "You don't have the right to delete an invoice." msgid "You don't have the right to delete an invoice."
msgstr "Vous n'avez pas le droit de supprimer une facture." msgstr "Vous n'avez pas le droit de supprimer une facture."
#: models.py:186 #: models.py:187
msgid "You don't have the right to delete this user's invoices." msgid "You don't have the right to delete this user's invoices."
msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur." msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur."
#: models.py:189 #: models.py:191
msgid "" msgid ""
"You don't have the right to delete an invoice already controlled or " "You don't have the right to delete an invoice already controlled or "
"invalidated." "invalidated."
msgstr "" msgstr ""
"Vous n'avez pas le droit de supprimer une facture précedement contrôlée ou " "Vous n'avez pas le droit de supprimer une facture précédemment contrôlée ou "
"invalidée." "invalidée."
#: models.py:197 #: models.py:199
msgid "You don't have the right to view someone else's invoices history." msgid "You don't have the right to view someone else's invoices history."
msgstr "" msgstr ""
"Vous n'avez pas le droit de voir l'historique des factures d'un autre " "Vous n'avez pas le droit de voir l'historique des factures d'un autre "
"utilisateur." "utilisateur."
#: models.py:200 #: models.py:202
msgid "The invoice has been invalidated." msgid "The invoice has been invalidated."
msgstr "La facture a été invalidée." msgstr "La facture a été invalidée."
#: models.py:210 #: models.py:214
msgid "You don't have the right to edit the \"controlled\" state." msgid "You don't have the right to edit the \"controlled\" state."
msgstr "Vous n'avez pas le droit de modifier le statut \"contrôlé\"." msgstr "Vous n'avez pas le droit de modifier le statut \"contrôlé\"."
#: models.py:224 #: models.py:228
msgid "There are no payment method which you can use." msgid "There are no payment method which you can use."
msgstr "Il n'y a pas de moyen de paiement que vous puissiez utiliser." msgstr "Il n'y a pas de moyen de paiement que vous puissiez utiliser."
#: models.py:226 #: models.py:230
msgid "There are no article that you can buy." msgid "There are no article that you can buy."
msgstr "Il n'y a pas d'article que vous puissiez acheter." msgstr "Il n'y a pas d'article que vous puissiez acheter."
#: models.py:261 #: models.py:272
msgid "Can view a custom invoice object" msgid "Can view a custom invoice object"
msgstr "Peut voir un objet facture personnalisée" msgstr "Peut voir un objet facture personnalisée"
#: models.py:265 templates/cotisations/aff_custom_invoice.html:36 #: models.py:276 templates/cotisations/aff_cost_estimate.html:36
#: templates/cotisations/aff_custom_invoice.html:36
msgid "Recipient" msgid "Recipient"
msgstr "Destinataire" msgstr "Destinataire"
#: models.py:269 templates/cotisations/aff_paiement.html:33 #: models.py:280 templates/cotisations/aff_paiement.html:33
msgid "Payment type" msgid "Payment type"
msgstr "Type de paiement" msgstr "Type de paiement"
#: models.py:273 #: models.py:284
msgid "Address" msgid "Address"
msgstr "Adresse" msgstr "Adresse"
#: models.py:276 templates/cotisations/aff_custom_invoice.html:54 #: models.py:287 templates/cotisations/aff_custom_invoice.html:54
msgid "Paid" msgid "Paid"
msgstr "Payé" msgstr "Payé"
#: models.py:296 models.py:516 models.py:764 #: models.py:291
msgid "Remark"
msgstr "Remarque"
#: models.py:300
msgid "Can view a cost estimate object"
msgstr "Peut voir un objet devis"
#: models.py:303
msgid "Period of validity"
msgstr "Période de validité"
#: models.py:340
msgid "You don't have the right to delete a cost estimate."
msgstr "Vous n'avez pas le droit de supprimer un devis."
#: models.py:343
msgid "The cost estimate has an invoice and can't be deleted."
msgstr "Le devis a une facture et ne peut pas être supprimé."
#: models.py:364 models.py:585 models.py:852
msgid "Connection" msgid "Connection"
msgstr "Connexion" msgstr "Connexion"
#: models.py:297 models.py:517 models.py:765 #: models.py:365 models.py:586 models.py:853
msgid "Membership" msgid "Membership"
msgstr "Adhésion" msgstr "Adhésion"
#: models.py:298 models.py:512 models.py:518 models.py:766 #: models.py:366 models.py:581 models.py:587 models.py:854
msgid "Both of them" msgid "Both of them"
msgstr "Les deux" msgstr "Les deux"
#: models.py:310 #: models.py:378
msgid "amount" msgid "amount"
msgstr "montant" msgstr "montant"
#: models.py:315 #: models.py:383
msgid "article" msgid "article"
msgstr "article" msgstr "article"
#: models.py:322 #: models.py:390
msgid "price" msgid "price"
msgstr "prix" msgstr "prix"
#: models.py:327 models.py:535 #: models.py:395 models.py:604
msgid "duration (in months)" msgid "duration (in months)"
msgstr "durée (en mois)" msgstr "durée (en mois)"
#: models.py:335 models.py:549 models.py:780 #: models.py:403 models.py:618 models.py:868
msgid "subscription type" msgid "subscription type"
msgstr "type de cotisation" msgstr "type de cotisation"
#: models.py:340 #: models.py:408
msgid "Can view a purchase object" msgid "Can view a purchase object"
msgstr "Peut voir un objet achat" msgstr "Peut voir un objet achat"
#: models.py:341 #: models.py:409
msgid "Can edit all the previous purchases" msgid "Can edit all the previous purchases"
msgstr "Peut modifier tous les achats précédents" msgstr "Peut modifier tous les achats précédents"
#: models.py:343 models.py:774 #: models.py:411 models.py:862
msgid "purchase" msgid "purchase"
msgstr "achat" msgstr "achat"
#: models.py:344 #: models.py:412
msgid "purchases" msgid "purchases"
msgstr "achats" msgstr "achats"
#: models.py:411 models.py:573 #: models.py:479 models.py:642
msgid "Duration must be specified for a subscription." msgid "Duration must be specified for a subscription."
msgstr "La durée de la cotisation doit être indiquée." msgstr "La durée de la cotisation doit être indiquée."
#: models.py:418 #: models.py:486
msgid "You don't have the right to edit the purchases." msgid "You don't have the right to edit the purchases."
msgstr "Vous n'avez pas le droit de modifier les achats." msgstr "Vous n'avez pas le droit de modifier les achats."
#: models.py:423 #: models.py:491
msgid "You don't have the right to edit this user's purchases." msgid "You don't have the right to edit this user's purchases."
msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur." msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur."
#: models.py:427 #: models.py:495
msgid "" msgid ""
"You don't have the right to edit a purchase already controlled or " "You don't have the right to edit a purchase already controlled or "
"invalidated." "invalidated."
@ -285,150 +325,150 @@ msgstr ""
"Vous n'avez pas le droit de modifier un achat précédemment contrôlé ou " "Vous n'avez pas le droit de modifier un achat précédemment contrôlé ou "
"invalidé." "invalidé."
#: models.py:434 #: models.py:502
msgid "You don't have the right to delete a purchase." msgid "You don't have the right to delete a purchase."
msgstr "Vous n'avez pas le droit de supprimer un achat." msgstr "Vous n'avez pas le droit de supprimer un achat."
#: models.py:436 #: models.py:504
msgid "You don't have the right to delete this user's purchases." msgid "You don't have the right to delete this user's purchases."
msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur." msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur."
#: models.py:439 #: models.py:507
msgid "" msgid ""
"You don't have the right to delete a purchase already controlled or " "You don't have the right to delete a purchase already controlled or "
"invalidated." "invalidated."
msgstr "" msgstr ""
"Vous n'avez pas le droit de supprimer un achat précédement contrôlé ou " "Vous n'avez pas le droit de supprimer un achat précédemment contrôlé ou "
"invalidé." "invalidé."
#: models.py:447 #: models.py:515
msgid "You don't have the right to view someone else's purchase history." msgid "You don't have the right to view someone else's purchase history."
msgstr "" msgstr ""
"Vous n'avez pas le droit de voir l'historique des achats d'un autre " "Vous n'avez pas le droit de voir l'historique des achats d'un autre "
"utilisateur." "utilisateur."
#: models.py:511 #: models.py:580
msgid "Club" msgid "Club"
msgstr "Club" msgstr "Club"
#: models.py:523 #: models.py:592
msgid "designation" msgid "designation"
msgstr "désignation" msgstr "désignation"
#: models.py:529 #: models.py:598
msgid "unit price" msgid "unit price"
msgstr "prix unitaire" msgstr "prix unitaire"
#: models.py:541 #: models.py:610
msgid "type of users concerned" msgid "type of users concerned"
msgstr "type d'utilisateurs concernés" msgstr "type d'utilisateurs concernés"
#: models.py:553 models.py:649 #: models.py:622 models.py:733
msgid "is available for every user" msgid "is available for every user"
msgstr "est disponible pour chaque utilisateur" msgstr "est disponible pour chaque utilisateur"
#: models.py:560 #: models.py:629
msgid "Can view an article object" msgid "Can view an article object"
msgstr "Peut voir un objet article" msgstr "Peut voir un objet article"
#: models.py:561 #: models.py:630
msgid "Can buy every article" msgid "Can buy every article"
msgstr "Peut acheter chaque article" msgstr "Peut acheter chaque article"
#: models.py:569 #: models.py:638
msgid "Balance is a reserved article name." msgid "Balance is a reserved article name."
msgstr "Solde est un nom d'article réservé." msgstr "Solde est un nom d'article réservé."
#: models.py:594 #: models.py:663
msgid "You can't buy this article." msgid "You can't buy this article."
msgstr "Vous ne pouvez pas acheter cet article." msgstr "Vous ne pouvez pas acheter cet article."
#: models.py:624 #: models.py:708
msgid "Can view a bank object" msgid "Can view a bank object"
msgstr "Peut voir un objet banque" msgstr "Peut voir un objet banque"
#: models.py:626 #: models.py:710
msgid "bank" msgid "bank"
msgstr "banque" msgstr "banque"
#: models.py:627 #: models.py:711
msgid "banks" msgid "banks"
msgstr "banques" msgstr "banques"
#: models.py:645 #: models.py:729
msgid "method" msgid "method"
msgstr "moyen" msgstr "moyen"
#: models.py:654 #: models.py:738
msgid "is user balance" msgid "is user balance"
msgstr "est solde utilisateur" msgstr "est solde utilisateur"
#: models.py:655 #: models.py:739
msgid "There should be only one balance payment method." msgid "There should be only one balance payment method."
msgstr "Il ne devrait y avoir qu'un moyen de paiement solde." msgstr "Il ne devrait y avoir qu'un moyen de paiement solde."
#: models.py:661 #: models.py:745
msgid "Can view a payment method object" msgid "Can view a payment method object"
msgstr "Peut voir un objet moyen de paiement" msgstr "Peut voir un objet moyen de paiement"
#: models.py:662 #: models.py:746
msgid "Can use every payment method" msgid "Can use every payment method"
msgstr "Peut utiliser chaque moyen de paiement" msgstr "Peut utiliser chaque moyen de paiement"
#: models.py:664 #: models.py:748
msgid "payment method" msgid "payment method"
msgstr "moyen de paiement" msgstr "moyen de paiement"
#: models.py:665 #: models.py:749
msgid "payment methods" msgid "payment methods"
msgstr "moyens de paiement" msgstr "moyens de paiement"
#: models.py:699 payment_methods/comnpay/views.py:63 #: models.py:787 payment_methods/comnpay/views.py:63
#, python-format #, python-format
msgid "The subscription of %(member_name)s was extended to %(end_date)s." msgid "The subscription of %(member_name)s was extended to %(end_date)s."
msgstr "La cotisation de %(member_name)s a été étendue au %(end_date)s." msgstr "La cotisation de %(member_name)s a été étendue au %(end_date)s."
#: models.py:709 #: models.py:797
msgid "The invoice was created." msgid "The invoice was created."
msgstr "La facture a été créée." msgstr "La facture a été créée."
#: models.py:730 #: models.py:818
msgid "You can't use this payment method." msgid "You can't use this payment method."
msgstr "Vous ne pouvez pas utiliser ce moyen de paiement." msgstr "Vous ne pouvez pas utiliser ce moyen de paiement."
#: models.py:748 #: models.py:836
msgid "No custom payment method." msgid "No custom payment method."
msgstr "Pas de moyen de paiement personnalisé." msgstr "Pas de moyen de paiement personnalisé."
#: models.py:783 #: models.py:871
msgid "start date" msgid "start date"
msgstr "date de début" msgstr "date de début"
#: models.py:786 #: models.py:874
msgid "end date" msgid "end date"
msgstr "date de fin" msgstr "date de fin"
#: models.py:791 #: models.py:879
msgid "Can view a subscription object" msgid "Can view a subscription object"
msgstr "Peut voir un objet cotisation" msgstr "Peut voir un objet cotisation"
#: models.py:792 #: models.py:880
msgid "Can edit the previous subscriptions" msgid "Can edit the previous subscriptions"
msgstr "Peut modifier les cotisations précédentes" msgstr "Peut modifier les cotisations précédentes"
#: models.py:794 #: models.py:882
msgid "subscription" msgid "subscription"
msgstr "cotisation" msgstr "cotisation"
#: models.py:795 #: models.py:883
msgid "subscriptions" msgid "subscriptions"
msgstr "cotisations" msgstr "cotisations"
#: models.py:799 #: models.py:887
msgid "You don't have the right to edit a subscription." msgid "You don't have the right to edit a subscription."
msgstr "Vous n'avez pas le droit de modifier une cotisation." msgstr "Vous n'avez pas le droit de modifier une cotisation."
#: models.py:803 #: models.py:891
msgid "" msgid ""
"You don't have the right to edit a subscription already controlled or " "You don't have the right to edit a subscription already controlled or "
"invalidated." "invalidated."
@ -436,11 +476,11 @@ msgstr ""
"Vous n'avez pas le droit de modifier une cotisation précédemment contrôlée " "Vous n'avez pas le droit de modifier une cotisation précédemment contrôlée "
"ou invalidée." "ou invalidée."
#: models.py:810 #: models.py:898
msgid "You don't have the right to delete a subscription." msgid "You don't have the right to delete a subscription."
msgstr "Vous n'avez pas le droit de supprimer une cotisation." msgstr "Vous n'avez pas le droit de supprimer une cotisation."
#: models.py:813 #: models.py:901
msgid "" msgid ""
"You don't have the right to delete a subscription already controlled or " "You don't have the right to delete a subscription already controlled or "
"invalidated." "invalidated."
@ -448,7 +488,7 @@ msgstr ""
"Vous n'avez pas le droit de supprimer une cotisation précédemment contrôlée " "Vous n'avez pas le droit de supprimer une cotisation précédemment contrôlée "
"ou invalidée." "ou invalidée."
#: models.py:821 #: models.py:909
msgid "You don't have the right to view someone else's subscription history." msgid "You don't have the right to view someone else's subscription history."
msgstr "" msgstr ""
"Vous n'avez pas le droit de voir l'historique des cotisations d'un autre " "Vous n'avez pas le droit de voir l'historique des cotisations d'un autre "
@ -482,11 +522,11 @@ msgstr "Le montant maximal d'argent autorisé pour le solde."
msgid "Allow user to credit their balance" msgid "Allow user to credit their balance"
msgstr "Autorise l'utilisateur à créditer son solde" msgstr "Autorise l'utilisateur à créditer son solde"
#: payment_methods/balance/models.py:81 payment_methods/balance/models.py:112 #: payment_methods/balance/models.py:79 payment_methods/balance/models.py:110
msgid "Your balance is too low for this operation." msgid "Your balance is too low for this operation."
msgstr "Votre solde est trop bas pour cette opération." msgstr "Votre solde est trop bas pour cette opération."
#: payment_methods/balance/models.py:99 validators.py:20 #: payment_methods/balance/models.py:97 validators.py:20
msgid "There is already a payment method for user balance." msgid "There is already a payment method for user balance."
msgstr "Il y a déjà un moyen de paiement pour le solde utilisateur." msgstr "Il y a déjà un moyen de paiement pour le solde utilisateur."
@ -523,11 +563,11 @@ msgstr ""
msgid "Production mode enabled (production URL, instead of homologation)" msgid "Production mode enabled (production URL, instead of homologation)"
msgstr "Mode production activé (URL de production, au lieu d'homologation)" msgstr "Mode production activé (URL de production, au lieu d'homologation)"
#: payment_methods/comnpay/models.py:104 #: payment_methods/comnpay/models.py:102
msgid "Pay invoice number " msgid "Pay invoice number "
msgstr "Payer la facture numéro " msgstr "Payer la facture numéro "
#: payment_methods/comnpay/models.py:116 #: payment_methods/comnpay/models.py:114
msgid "" msgid ""
"In order to pay your invoice with ComNpay, the price must be greater than {} " "In order to pay your invoice with ComNpay, the price must be greater than {} "
"€." "€."
@ -559,6 +599,30 @@ msgstr ""
msgid "no" msgid "no"
msgstr "non" msgstr "non"
#: payment_methods/note_kfet/forms.py:32
msgid "pseudo note"
msgstr "pseudo note"
#: payment_methods/note_kfet/forms.py:35
msgid "Password"
msgstr "Mot de passe"
#: payment_methods/note_kfet/models.py:40
msgid "NoteKfet"
msgstr "NoteKfet"
#: payment_methods/note_kfet/models.py:50
msgid "server"
msgstr "serveur"
#: payment_methods/note_kfet/views.py:60
msgid "Unknown error."
msgstr "Erreur inconnue."
#: payment_methods/note_kfet/views.py:88
msgid "The payment with note was done."
msgstr "Le paiement par note a été effectué."
#: templates/cotisations/aff_article.html:34 #: templates/cotisations/aff_article.html:34
msgid "Price" msgid "Price"
msgstr "Prix" msgstr "Prix"
@ -579,34 +643,47 @@ msgstr "Utilisateurs concernés"
msgid "Available for everyone" msgid "Available for everyone"
msgstr "Disponible pour tous" msgstr "Disponible pour tous"
#: templates/cotisations/aff_article.html:52
#: templates/cotisations/aff_paiement.html:48
#: templates/cotisations/control.html:107 views.py:483 views.py:570
#: views.py:650
msgid "Edit"
msgstr "Modifier"
#: templates/cotisations/aff_banque.html:32 #: templates/cotisations/aff_banque.html:32
msgid "Bank" msgid "Bank"
msgstr "Banque" msgstr "Banque"
#: templates/cotisations/aff_cotisations.html:38 #: templates/cotisations/aff_cost_estimate.html:39
msgid "User"
msgstr "Utilisateur"
#: templates/cotisations/aff_cotisations.html:41 #: templates/cotisations/aff_cotisations.html:41
#: templates/cotisations/aff_custom_invoice.html:39 #: templates/cotisations/aff_custom_invoice.html:39
#: templates/cotisations/control.html:63 #: templates/cotisations/control.html:63
#: templates/cotisations/edit_facture.html:45 #: templates/cotisations/edit_facture.html:49
msgid "Designation" msgid "Designation"
msgstr "Désignation" msgstr "Désignation"
#: templates/cotisations/aff_cost_estimate.html:40
#: templates/cotisations/aff_cotisations.html:42 #: templates/cotisations/aff_cotisations.html:42
#: templates/cotisations/aff_custom_invoice.html:40 #: templates/cotisations/aff_custom_invoice.html:40
#: templates/cotisations/control.html:64 #: templates/cotisations/control.html:64
msgid "Total price" msgid "Total price"
msgstr "Prix total" msgstr "Prix total"
#: templates/cotisations/aff_cost_estimate.html:50
msgid "Validity"
msgstr "Validité"
#: templates/cotisations/aff_cost_estimate.html:54
msgid "Cost estimate ID"
msgstr "ID devis"
#: templates/cotisations/aff_cost_estimate.html:58
msgid "Invoice created"
msgstr "Facture créée"
#: templates/cotisations/aff_cost_estimate.html:91
#: templates/cotisations/aff_cotisations.html:81
#: templates/cotisations/aff_custom_invoice.html:79
msgid "PDF"
msgstr "PDF"
#: templates/cotisations/aff_cotisations.html:38
msgid "User"
msgstr "Utilisateur"
#: templates/cotisations/aff_cotisations.html:52 #: templates/cotisations/aff_cotisations.html:52
#: templates/cotisations/aff_custom_invoice.html:50 #: templates/cotisations/aff_custom_invoice.html:50
#: templates/cotisations/control.html:56 #: templates/cotisations/control.html:56
@ -617,11 +694,6 @@ msgstr "ID facture"
msgid "Controlled invoice" msgid "Controlled invoice"
msgstr "Facture contrôlée" msgstr "Facture contrôlée"
#: templates/cotisations/aff_cotisations.html:81
#: templates/cotisations/aff_custom_invoice.html:79
msgid "PDF"
msgstr "PDF"
#: templates/cotisations/aff_cotisations.html:84 #: templates/cotisations/aff_cotisations.html:84
msgid "Invalidated invoice" msgid "Invalidated invoice"
msgstr "Facture invalidée" msgstr "Facture invalidée"
@ -666,6 +738,11 @@ msgstr "Validé"
msgid "Controlled" msgid "Controlled"
msgstr "Contrôlé" msgstr "Contrôlé"
#: templates/cotisations/control.html:107 views.py:642 views.py:729
#: views.py:809
msgid "Edit"
msgstr "Modifier"
#: templates/cotisations/delete.html:29 #: templates/cotisations/delete.html:29
msgid "Deletion of subscriptions" msgid "Deletion of subscriptions"
msgstr "Suppression de cotisations" msgstr "Suppression de cotisations"
@ -676,11 +753,12 @@ msgid ""
"Warning: are you sure you really want to delete this %(object_name)s object " "Warning: are you sure you really want to delete this %(object_name)s object "
"( %(objet)s )?" "( %(objet)s )?"
msgstr "" msgstr ""
"\tAttention: voulez-vous vraiment supprimer cet objet %(object_name)s " "Attention: voulez-vous vraiment supprimer cet objet %(object_name)s "
"( %(objet)s ) ?" "( %(objet)s ) ?"
#: templates/cotisations/delete.html:38 #: templates/cotisations/delete.html:38
#: templates/cotisations/edit_facture.html:60 #: templates/cotisations/edit_facture.html:64 views.py:178 views.py:228
#: views.py:280
msgid "Confirm" msgid "Confirm"
msgstr "Confirmer" msgstr "Confirmer"
@ -689,18 +767,19 @@ msgstr "Confirmer"
msgid "Creation and editing of invoices" msgid "Creation and editing of invoices"
msgstr "Création et modification de factures" msgstr "Création et modification de factures"
#: templates/cotisations/edit_facture.html:38 #: templates/cotisations/edit_facture.html:41
msgid "Edit the invoice" msgid "Edit invoice"
msgstr "Modifier la facture" msgstr "Modifier la facture"
#: templates/cotisations/edit_facture.html:41 #: templates/cotisations/edit_facture.html:45
#: templates/cotisations/facture.html:56 #: templates/cotisations/facture.html:62
msgid "Invoice's articles" #: templates/cotisations/index_article.html:30
msgstr "Articles de la facture" msgid "Articles"
msgstr "Articles"
#: templates/cotisations/facture.html:37 #: templates/cotisations/facture.html:37
msgid "New invoice" msgid "Buy"
msgstr "Nouvelle facture" msgstr "Acheter"
#: templates/cotisations/facture.html:40 #: templates/cotisations/facture.html:40
#, python-format #, python-format
@ -712,11 +791,11 @@ msgstr "Solde maximum autorisé : %(max_balance)s €"
msgid "Current balance: %(balance)s €" msgid "Current balance: %(balance)s €"
msgstr "Solde actuel : %(balance)s €" msgstr "Solde actuel : %(balance)s €"
#: templates/cotisations/facture.html:70 #: templates/cotisations/facture.html:76
msgid "Add an article" msgid "Add an extra article"
msgstr "Ajouter un article" msgstr "Ajouter un article supplémentaire"
#: templates/cotisations/facture.html:72 #: templates/cotisations/facture.html:82
msgid "Total price: <span id=\"total_price\">0,00</span> €" msgid "Total price: <span id=\"total_price\">0,00</span> €"
msgstr "Prix total : <span id=\"total_price\">0,00</span> €" msgstr "Prix total : <span id=\"total_price\">0,00</span> €"
@ -728,12 +807,8 @@ msgstr "Factures"
msgid "Subscriptions" msgid "Subscriptions"
msgstr "Cotisations" msgstr "Cotisations"
#: templates/cotisations/index_article.html:30
msgid "Articles"
msgstr "Articles"
#: templates/cotisations/index_article.html:33 #: templates/cotisations/index_article.html:33
msgid "Article types list" msgid "List of article types"
msgstr "Liste des types d'article" msgstr "Liste des types d'article"
#: templates/cotisations/index_article.html:36 #: templates/cotisations/index_article.html:36
@ -745,12 +820,12 @@ msgid "Delete one or several article types"
msgstr "Supprimer un ou plusieurs types d'article" msgstr "Supprimer un ou plusieurs types d'article"
#: templates/cotisations/index_banque.html:30 #: templates/cotisations/index_banque.html:30
#: templates/cotisations/sidebar.html:55 #: templates/cotisations/sidebar.html:60
msgid "Banks" msgid "Banks"
msgstr "Banques" msgstr "Banques"
#: templates/cotisations/index_banque.html:33 #: templates/cotisations/index_banque.html:33
msgid "Banks list" msgid "List of banks"
msgstr "Liste des banques" msgstr "Liste des banques"
#: templates/cotisations/index_banque.html:36 #: templates/cotisations/index_banque.html:36
@ -761,17 +836,26 @@ msgstr "Ajouter une banque"
msgid "Delete one or several banks" msgid "Delete one or several banks"
msgstr "Supprimer une ou plusieurs banques" msgstr "Supprimer une ou plusieurs banques"
#: templates/cotisations/index_cost_estimate.html:28
#: templates/cotisations/sidebar.html:50
msgid "Cost estimates"
msgstr "Devis"
#: templates/cotisations/index_cost_estimate.html:31
msgid "List of cost estimates"
msgstr "Liste des devis"
#: templates/cotisations/index_custom_invoice.html:28 #: templates/cotisations/index_custom_invoice.html:28
#: templates/cotisations/sidebar.html:45 #: templates/cotisations/sidebar.html:45
msgid "Custom invoices" msgid "Custom invoices"
msgstr "Factures personnalisées" msgstr "Factures personnalisées"
#: templates/cotisations/index_custom_invoice.html:31 #: templates/cotisations/index_custom_invoice.html:31
msgid "Custom invoices list" msgid "List of custom invoices"
msgstr "Liste des factures personalisées" msgstr "Liste des factures personnalisées"
#: templates/cotisations/index_paiement.html:30 #: templates/cotisations/index_paiement.html:30
#: templates/cotisations/sidebar.html:60 #: templates/cotisations/sidebar.html:65
msgid "Payment methods" msgid "Payment methods"
msgstr "Moyens de paiement" msgstr "Moyens de paiement"
@ -794,9 +878,9 @@ msgstr "Rechargement de solde"
#: templates/cotisations/payment.html:34 #: templates/cotisations/payment.html:34
#, python-format #, python-format
msgid "Pay %(amount)s €" msgid "Pay %(amount)s €"
msgstr "Recharger de %(amount)s €" msgstr "Payer %(amount)s €"
#: templates/cotisations/payment.html:42 views.py:870 #: templates/cotisations/payment.html:42 views.py:1049
msgid "Pay" msgid "Pay"
msgstr "Payer" msgstr "Payer"
@ -808,84 +892,104 @@ msgstr "Créer une facture"
msgid "Control the invoices" msgid "Control the invoices"
msgstr "Contrôler les factures" msgstr "Contrôler les factures"
#: views.py:167 #: views.py:164
msgid "You need to choose at least one article." msgid "You need to choose at least one article."
msgstr "Vous devez choisir au moins un article." msgstr "Vous devez choisir au moins un article."
#: views.py:181 views.py:235 #: views.py:222
msgid "Create" msgid "The cost estimate was created."
msgstr "Créer" msgstr "Le devis a été créé."
#: views.py:228 #: views.py:232 views.py:534
msgid "Cost estimate"
msgstr "Devis"
#: views.py:274
msgid "The custom invoice was created." msgid "The custom invoice was created."
msgstr "La facture personnalisée a été créée." msgstr "La facture personnalisée a été créée."
#: views.py:316 views.py:370 #: views.py:363 views.py:466
msgid "The invoice was edited." msgid "The invoice was edited."
msgstr "La facture a été modifiée." msgstr "La facture a été modifiée."
#: views.py:336 views.py:430 #: views.py:383 views.py:589
msgid "The invoice was deleted." msgid "The invoice was deleted."
msgstr "La facture a été supprimée." msgstr "La facture a été supprimée."
#: views.py:341 views.py:435 #: views.py:388 views.py:594
msgid "Invoice" msgid "Invoice"
msgstr "Facture" msgstr "Facture"
#: views.py:456 #: views.py:417
msgid "The cost estimate was edited."
msgstr "Le devis a été modifié."
#: views.py:424
msgid "Edit cost estimate"
msgstr "Modifier le devis"
#: views.py:436
msgid "An invoice was successfully created from your cost estimate."
msgstr "Une facture a bien été créée à partir de votre devis."
#: views.py:529
msgid "The cost estimate was deleted."
msgstr "Le devis a été supprimé."
#: views.py:615
msgid "The article was created." msgid "The article was created."
msgstr "L'article a été créé." msgstr "L'article a été créé."
#: views.py:461 views.py:534 views.py:627 #: views.py:620 views.py:693 views.py:786
msgid "Add" msgid "Add"
msgstr "Ajouter" msgstr "Ajouter"
#: views.py:462 #: views.py:621
msgid "New article" msgid "New article"
msgstr "Nouvel article" msgstr "Nouvel article"
#: views.py:478 #: views.py:637
msgid "The article was edited." msgid "The article was edited."
msgstr "L'article a été modifié." msgstr "L'article a été modifié."
#: views.py:484 #: views.py:643
msgid "Edit article" msgid "Edit article"
msgstr "Modifier l'article" msgstr "Modifier l'article"
#: views.py:500 #: views.py:659
msgid "The articles were deleted." msgid "The articles were deleted."
msgstr "Les articles ont été supprimés." msgstr "Les articles ont été supprimés."
#: views.py:505 views.py:605 views.py:685 #: views.py:664 views.py:764 views.py:844
msgid "Delete" msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
#: views.py:506 #: views.py:665
msgid "Delete article" msgid "Delete article"
msgstr "Supprimer l'article" msgstr "Supprimer l'article"
#: views.py:528 #: views.py:687
msgid "The payment method was created." msgid "The payment method was created."
msgstr "Le moyen de paiment a été créé." msgstr "Le moyen de paiment a été créé."
#: views.py:535 #: views.py:694
msgid "New payment method" msgid "New payment method"
msgstr "Nouveau moyen de paiement" msgstr "Nouveau moyen de paiement"
#: views.py:564 #: views.py:723
msgid "The payment method was edited." msgid "The payment method was edited."
msgstr "Le moyen de paiment a été modifié." msgstr "Le moyen de paiment a été modifié."
#: views.py:571 #: views.py:730
msgid "Edit payment method" msgid "Edit payment method"
msgstr "Modifier le moyen de paiement" msgstr "Modifier le moyen de paiement"
#: views.py:590 #: views.py:749
#, python-format #, python-format
msgid "The payment method %(method_name)s was deleted." msgid "The payment method %(method_name)s was deleted."
msgstr "Le moyen de paiement %(method_name)s a été supprimé." msgstr "Le moyen de paiement %(method_name)s a été supprimé."
#: views.py:597 #: views.py:756
#, python-format #, python-format
msgid "" msgid ""
"The payment method %(method_name)s can't be deleted " "The payment method %(method_name)s can't be deleted "
@ -894,52 +998,51 @@ msgstr ""
"Le moyen de paiement %(method_name)s ne peut pas être supprimé car il y a " "Le moyen de paiement %(method_name)s ne peut pas être supprimé car il y a "
"des factures qui l'utilisent." "des factures qui l'utilisent."
#: views.py:606 #: views.py:765
msgid "Delete payment method" msgid "Delete payment method"
msgstr "Supprimer le moyen de paiement" msgstr "Supprimer le moyen de paiement"
#: views.py:622 #: views.py:781
msgid "The bank was created." msgid "The bank was created."
msgstr "La banque a été créée." msgstr "La banque a été créée."
#: views.py:628 #: views.py:787
msgid "New bank" msgid "New bank"
msgstr "Nouvelle banque" msgstr "Nouvelle banque"
#: views.py:645 #: views.py:804
msgid "The bank was edited." msgid "The bank was edited."
msgstr "La banque a été modifiée." msgstr "La banque a été modifiée."
#: views.py:651 #: views.py:810
msgid "Edit bank" msgid "Edit bank"
msgstr "Modifier la banque" msgstr "Modifier la banque"
#: views.py:670 #: views.py:829
#, python-format #, python-format
msgid "The bank %(bank_name)s was deleted." msgid "The bank %(bank_name)s was deleted."
msgstr "La banque %(bank_name)s a été supprimée." msgstr "La banque %(bank_name)s a été supprimée."
#: views.py:677 #: views.py:836
#, python-format #, python-format
msgid "" msgid ""
"The bank %(bank_name)s can't be deleted because there " "The bank %(bank_name)s can't be deleted because there are invoices using it."
"are invoices using it."
msgstr "" msgstr ""
"La banque %(bank_name)s ne peut pas être supprimée car il y a des factures " "La banque %(bank_name)s ne peut pas être supprimée car il y a des factures "
"qui l'utilisent." "qui l'utilisent."
#: views.py:686 #: views.py:845
msgid "Delete bank" msgid "Delete bank"
msgstr "Supprimer la banque" msgstr "Supprimer la banque"
#: views.py:722 #: views.py:881
msgid "Your changes have been properly taken into account." msgid "Your changes have been properly taken into account."
msgstr "Vos modifications ont correctement été prises en compte." msgstr "Vos modifications ont correctement été prises en compte."
#: views.py:834 #: views.py:1016
msgid "You are not allowed to credit your balance." msgid "You are not allowed to credit your balance."
msgstr "Vous n'êtes pas autorisés à créditer votre solde." msgstr "Vous n'êtes pas autorisés à créditer votre solde."
#: views.py:869 #: views.py:1048
msgid "Refill your balance" msgid "Refill your balance"
msgstr "Recharger votre solde" msgstr "Recharger votre solde"

View file

@ -30,7 +30,9 @@ def update_rights(apps, schema_editor):
create_permissions(app) create_permissions(app)
app.models_module = False app.models_module = False
former = Permission.objects.get(codename='change_facture_pdf') ContentType = apps.get_model("contenttypes", "ContentType")
content_type = ContentType.objects.get_for_model(Permission)
former, created = Permission.objects.get_or_create(codename='change_facture_pdf', content_type=content_type)
new_1 = Permission.objects.get(codename='add_custominvoice') new_1 = Permission.objects.get(codename='add_custominvoice')
new_2 = Permission.objects.get(codename='change_custominvoice') new_2 = Permission.objects.get(codename='change_custominvoice')
new_3 = Permission.objects.get(codename='view_custominvoice') new_3 = Permission.objects.get(codename='view_custominvoice')

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-31 13:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0033_auto_20180818_1319'),
]
operations = [
migrations.AlterField(
model_name='facture',
name='valid',
field=models.BooleanField(default=False, verbose_name='validated'),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-09-01 11:27
from __future__ import unicode_literals
import cotisations.payment_methods.mixins
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0034_auto_20180831_1532'),
]
operations = [
migrations.CreateModel(
name='NotePayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('server', models.CharField(max_length=255, verbose_name='server')),
('port', models.PositiveIntegerField(blank=True, null=True)),
('id_note', models.PositiveIntegerField(blank=True, null=True)),
('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')),
],
options={
'verbose_name': 'NoteKfet',
},
bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-29 14:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0035_notepayment'),
]
operations = [
migrations.AddField(
model_name='custominvoice',
name='remark',
field=models.TextField(blank=True, null=True, verbose_name='Remark'),
),
]

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-29 21:03
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0036_custominvoice_remark'),
]
operations = [
migrations.CreateModel(
name='CostEstimate',
fields=[
('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')),
('validity', models.DurationField(verbose_name='Period of validity')),
('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')),
],
options={
'permissions': (('view_costestimate', 'Can view a cost estimate object'),),
},
bases=('cotisations.custominvoice',),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-31 22:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0037_costestimate'),
]
operations = [
migrations.AlterField(
model_name='costestimate',
name='final_invoice',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'),
),
migrations.AlterField(
model_name='costestimate',
name='validity',
field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'),
),
migrations.AlterField(
model_name='custominvoice',
name='paid',
field=models.BooleanField(default=False, verbose_name='Paid'),
),
]

View file

@ -46,11 +46,14 @@ from django.urls import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib import messages from django.contrib import messages
from preferences.models import CotisationsOption
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.utils import (
find_payment_method, send_mail_invoice, send_mail_voucher
)
from cotisations.validators import check_no_balance from cotisations.validators import check_no_balance
@ -83,7 +86,7 @@ class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
).aggregate( ).aggregate(
total=models.Sum( total=models.Sum(
models.F('prix')*models.F('number'), models.F('prix')*models.F('number'),
output_field=models.FloatField() output_field=models.DecimalField()
) )
)['total'] or 0 )['total'] or 0
@ -137,7 +140,7 @@ class Facture(BaseInvoice):
) )
# TODO : change name to validity for clarity # TODO : change name to validity for clarity
valid = models.BooleanField( valid = models.BooleanField(
default=True, default=False,
verbose_name=_("validated") verbose_name=_("validated")
) )
# TODO : changed name to controlled for clarity # TODO : changed name to controlled for clarity
@ -182,24 +185,28 @@ class Facture(BaseInvoice):
def can_delete(self, user_request, *args, **kwargs): def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_facture'): if not user_request.has_perm('cotisations.delete_facture'):
return False, _("You don't have the right to delete an invoice.") return False, _("You don't have the right to delete an invoice.")
if not self.user.can_edit(user_request, *args, **kwargs)[0]: elif not user_request.has_perm('cotisations.change_all_facture') and \
not self.user.can_edit(user_request, *args, **kwargs)[0]:
return False, _("You don't have the right to delete this user's " return False, _("You don't have the right to delete this user's "
"invoices.") "invoices.")
if self.control or not self.valid: elif not user_request.has_perm('cotisations.change_all_facture') and \
(self.control or not self.valid):
return False, _("You don't have the right to delete an invoice " return False, _("You don't have the right to delete an invoice "
"already controlled or invalidated.") "already controlled or invalidated.")
else: else:
return True, None return True, None
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
if not user_request.has_perm('cotisations.view_facture') and \ if not user_request.has_perm('cotisations.view_facture'):
self.user != user_request: if self.user != user_request:
return False, _("You don't have the right to view someone else's " return False, _("You don't have the right to view someone else's "
"invoices history.") "invoices history.")
elif not self.valid: elif not self.valid:
return False, _("The invoice has been invalidated.") return False, _("The invoice has been invalidated.")
else: else:
return True, None return True, None
else:
return True, None
@staticmethod @staticmethod
def can_change_control(user_request, *_args, **_kwargs): def can_change_control(user_request, *_args, **_kwargs):
@ -231,6 +238,31 @@ class Facture(BaseInvoice):
self.field_permissions = { self.field_permissions = {
'control': self.can_change_control, 'control': self.can_change_control,
} }
self.__original_valid = self.valid
self.__original_control = self.control
def get_subscription(self):
"""Returns every subscription associated with this invoice."""
return Cotisation.objects.filter(
vente__in=self.vente_set.filter(
Q(type_cotisation='All') |
Q(type_cotisation='Adhesion')
)
)
def is_subscription(self):
"""Returns True if this invoice contains at least one subscribtion."""
return bool(self.get_subscription())
def save(self, *args, **kwargs):
super(Facture, self).save(*args, **kwargs)
if not self.__original_valid and self.valid:
send_mail_invoice(self)
if self.is_subscription() \
and not self.__original_control \
and self.control \
and CotisationsOption.get_cached_value('send_voucher_mail'):
send_mail_voucher(self)
def __str__(self): def __str__(self):
return str(self.user) + ' ' + str(self.date) return str(self.user) + ' ' + str(self.date)
@ -242,7 +274,9 @@ def facture_post_save(**kwargs):
Synchronise the LDAP user after an invoice has been saved. Synchronise the LDAP user after an invoice has been saved.
""" """
facture = kwargs['instance'] facture = kwargs['instance']
if facture.valid:
user = facture.user user = facture.user
user.set_active()
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@ -273,8 +307,65 @@ class CustomInvoice(BaseInvoice):
verbose_name=_("Address") verbose_name=_("Address")
) )
paid = models.BooleanField( paid = models.BooleanField(
verbose_name=_("Paid") verbose_name=_("Paid"),
default=False
) )
remark = models.TextField(
verbose_name=_("Remark"),
blank=True,
null=True
)
class CostEstimate(CustomInvoice):
class Meta:
permissions = (
('view_costestimate', _("Can view a cost estimate object")),
)
validity = models.DurationField(
verbose_name=_("Period of validity"),
help_text="DD HH:MM:SS"
)
final_invoice = models.ForeignKey(
CustomInvoice,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="origin_cost_estimate",
primary_key=False
)
def create_invoice(self):
"""Create a CustomInvoice from the CostEstimate."""
if self.final_invoice is not None:
return self.final_invoice
invoice = CustomInvoice()
invoice.recipient = self.recipient
invoice.payment = self.payment
invoice.address = self.address
invoice.paid = False
invoice.remark = self.remark
invoice.date = timezone.now()
invoice.save()
self.final_invoice = invoice
self.save()
for sale in self.vente_set.all():
Vente.objects.create(
facture=invoice,
name=sale.name,
prix=sale.prix,
number=sale.number,
)
return invoice
def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_costestimate'):
return False, _("You don't have the right "
"to delete a cost estimate.")
if self.final_invoice is not None:
return False, _("The cost estimate has an "
"invoice and can't be deleted.")
return True, None
# TODO : change Vente to Purchase # TODO : change Vente to Purchase
@ -471,8 +562,9 @@ def vente_post_save(**kwargs):
if purchase.type_cotisation: if purchase.type_cotisation:
purchase.create_cotis() purchase.create_cotis()
purchase.cotisation.save() purchase.cotisation.save()
user = purchase.facture.user user = purchase.facture.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.set_active()
user.ldap_sync(base=True, access_refresh=True, mac_refresh=False)
# TODO : change vente to purchase # TODO : change vente to purchase
@ -602,7 +694,9 @@ class Article(RevMixin, AclMixin, models.Model):
user: The user requesting articles. user: The user requesting articles.
target_user: The user to sell articles target_user: The user to sell articles
""" """
if target_user.is_class_club: if target_user is None:
objects_pool = cls.objects.filter(Q(type_user='All'))
elif target_user.is_class_club:
objects_pool = cls.objects.filter( objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Club') Q(type_user='All') | Q(type_user='Club')
) )
@ -610,6 +704,10 @@ class Article(RevMixin, AclMixin, models.Model):
objects_pool = cls.objects.filter( objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Adherent') Q(type_user='All') | Q(type_user='Adherent')
) )
if target_user is not None and not target_user.is_adherent():
objects_pool = objects_pool.filter(
Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
)
if user.has_perm('cotisations.buy_every_article'): if user.has_perm('cotisations.buy_every_article'):
return objects_pool return objects_pool
return objects_pool.filter(available_for_everyone=True) return objects_pool.filter(available_for_everyone=True)
@ -700,6 +798,10 @@ class Paiement(RevMixin, AclMixin, models.Model):
if payment_method is not None and use_payment_method: if payment_method is not None and use_payment_method:
return payment_method.end_payment(invoice, request) return payment_method.end_payment(invoice, request)
# So make this invoice valid, trigger send mail
invoice.valid = True
invoice.save()
# In case a cotisation was bought, inform the user, the # In case a cotisation was bought, inform the user, the
# cotisation time has been extended too # cotisation time has been extended too
if any(sell.type_cotisation for sell in invoice.vente_set.all()): if any(sell.type_cotisation for sell in invoice.vente_set.all()):
@ -856,4 +958,3 @@ def cotisation_post_delete(**_kwargs):
""" """
regen('mac_ip_list') regen('mac_ip_list')
regen('mailing') regen('mailing')

View file

@ -127,10 +127,11 @@ method to your model, where `form` is an instance of
""" """
from . import comnpay, cheque, balance, urls from . import comnpay, cheque, balance, note_kfet, urls
PAYMENT_METHODS = [ PAYMENT_METHODS = [
comnpay, comnpay,
cheque, cheque,
balance, balance,
note_kfet
] ]

View file

@ -73,9 +73,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
""" """
user = invoice.user user = invoice.user
total_price = invoice.prix_total() total_price = invoice.prix_total()
if float(user.solde) - float(total_price) < self.minimum_balance: if user.solde - total_price < self.minimum_balance:
invoice.valid = False
invoice.save()
messages.error( messages.error(
request, request,
_("Your balance is too low for this operation.") _("Your balance is too low for this operation.")
@ -108,7 +106,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
balance. balance.
""" """
return ( return (
float(user.solde) - float(price) >= self.minimum_balance, user.solde - price >= self.minimum_balance,
_("Your balance is too low for this operation.") _("Your balance is too low for this operation.")
) )

View file

@ -46,8 +46,6 @@ class ChequePayment(PaymentMethodMixin, models.Model):
"""Invalidates the invoice then redirect the user towards a view asking """Invalidates the invoice then redirect the user towards a view asking
for informations to add to the invoice before validating it. for informations to add to the invoice before validating it.
""" """
invoice.valid = False
invoice.save()
return redirect(reverse( return redirect(reverse(
'cotisations:cheque:validate', 'cotisations:cheque:validate',
kwargs={'invoice_pk': invoice.pk} kwargs={'invoice_pk': invoice.pk}

View file

@ -81,8 +81,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
a facture id, the price and the secret transaction data stored in a facture id, the price and the secret transaction data stored in
the preferences. the preferences.
""" """
invoice.valid = False
invoice.save()
host = request.get_host() host = request.get_host()
p = Transaction( p = Transaction(
str(self.payment_credential), str(self.payment_credential),

View file

@ -62,13 +62,13 @@ def accept_payment(request, factureid):
request, request,
_("The subscription of %(member_name)s was extended to" _("The subscription of %(member_name)s was extended to"
" %(end_date)s.") % { " %(end_date)s.") % {
'member_name': request.user.pseudo, 'member_name': invoice.user.pseudo,
'end_date': request.user.end_adhesion() 'end_date': invoice.user.end_adhesion()
} }
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': request.user.id} kwargs={'userid': invoice.user.id}
)) ))

View 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 Gabriel Detraz, Pierre-Antoine Comby
#
# 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
NAME = "NOTE"
PaymentMethod = models.NotePayment

View file

@ -0,0 +1,38 @@
# -*- 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 Pierre-Antoine Comby
# Copyright © 2018 Gabriel Detraz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from django import forms
from django.utils.translation import ugettext_lazy as _
from cotisations.utils import find_payment_method
class NoteCredentialForm(forms.Form):
"""A special form to get credential to connect to a NoteKfet2015 server throught his API
object.
"""
login = forms.CharField(
label=_("pseudo note")
)
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput
)

View file

@ -0,0 +1,65 @@
# -*- 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 Pierre-Antoine Comby
# Copyright © 2018 Gabriel Detraz
#
# 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_lazy as _
from django.contrib import messages
from cotisations.models import Paiement
from cotisations.payment_methods.mixins import PaymentMethodMixin
from django.shortcuts import render, redirect
class NotePayment(PaymentMethodMixin, models.Model):
"""
The model allowing you to pay with NoteKfet2015.
"""
class Meta:
verbose_name = _("NoteKfet")
payment = models.OneToOneField(
Paiement,
on_delete = models.CASCADE,
related_name = 'payment_method',
editable = False
)
server = models.CharField(
max_length=255,
verbose_name=_("server")
)
port = models.PositiveIntegerField(
blank = True,
null = True
)
id_note = models.PositiveIntegerField(
blank = True,
null = True
)
def end_payment(self, invoice, request):
return redirect(reverse(
'cotisations:note_kfet:note_payment',
kwargs={'factureid': invoice.id}
))

View file

@ -0,0 +1,74 @@
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# Codé par PAC , forké de 20-100
""" Module pour dialoguer avec la NoteKfet2015 """
import socket
import json
import ssl
import traceback
def get_response(socket):
length_str = b''
char = socket.recv(1)
while char != b'\n':
length_str += char
char = socket.recv(1)
total = int(length_str)
return json.loads(socket.recv(total).decode('utf-8'))
def connect(server, port):
sock = socket.socket()
try:
# On établit la connexion sur port 4242
sock.connect((server, port))
# On passe en SSL
sock = ssl.wrap_socket(sock)
# On fait un hello
sock.send(b'["hello", "manual"]')
retcode = get_response(sock)
except:
# Si on a foiré quelque part, c'est que le serveur est down
return (False, sock, "Serveur indisponible")
return (True, sock, "")
def login(server, port, username, password, masque = [[], [], True]):
result, sock, err = connect(server, port)
if not result:
return (False, None, err)
try:
commande = ["login", [username, password, "bdd", masque]]
sock.send(json.dumps(commande).encode("utf-8"))
response = get_response(sock)
retcode = response['retcode']
if retcode == 0:
return (True, sock, "")
elif retcode == 5:
return (False, sock, "Login incorrect")
else:
return (False, sock, "Erreur inconnue " + str(retcode))
except:
# Si on a foiré quelque part, c'est que le serveur est down
return (False, sock, "Erreur de communication avec le serveur")
def don(sock, montant, id_note, facture):
"""
Faire faire un don à l'id_note
"""
try:
sock.send(json.dumps(["dons", [[id_note], round(montant*100), "Facture : id=%s, designation=%s" % (facture.id, facture.name())]]).encode("utf-8"))
response = get_response(sock)
retcode = response['retcode']
transaction_retcode = response["msg"][0][0]
if 0 < retcode < 100 or 200 <= retcode or 0 < transaction_retcode < 100 or 200 <= transaction_retcode:
return (False, "Transaction échouée. (Solde trop négatif ?)")
elif retcode == 0:
return (True, "")
else:
return (False, "Erreur inconnue " + str(retcode))
except:
return (False, "Erreur de communication avec le serveur")

View 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 Gabriel Detraz
#
# 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'^note_payment/(?P<factureid>[0-9]+)$',
views.note_payment,
name='note_payment'
),
]

View file

@ -0,0 +1,97 @@
# -*- 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 Gabriel Detraz
# Copyright © 2018 Pierre-Antoine Comby
#
# 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 the views needed by comnpay
"""
from collections import OrderedDict
from django.urls import reverse
from django.shortcuts import redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpResponseBadRequest
from cotisations.models import Facture
from cotisations.utils import find_payment_method
from .models import NotePayment
from re2o.views import form
from re2o.acl import (
can_create,
can_edit
)
from .note import login, don
from .forms import NoteCredentialForm
@login_required
@can_edit(Facture)
def note_payment(request, facture, factureid):
"""
Build a request to start the negociation with NoteKfet by using
a facture id, the price and the login/password data stored in
the preferences.
"""
user = facture.user
payment_method = find_payment_method(facture.paiement)
if not payment_method or not isinstance(payment_method, NotePayment):
messages.error(request, _("Unknown error."))
return redirect(reverse(
'users:profil',
kwargs={'userid': user.id}
))
noteform = NoteCredentialForm(request.POST or None)
if noteform.is_valid():
pseudo = noteform.cleaned_data['login']
password = noteform.cleaned_data['password']
result, sock, err = login(payment_method.server, payment_method.port, pseudo, password)
if not result:
messages.error(request, err)
return form(
{'form': noteform, 'amount': facture.prix_total()},
"cotisations/payment.html",
request
)
else:
result, err = don(sock, facture.prix_total(), payment_method.id_note, facture)
if not result:
messages.error(request, err)
return form(
{'form': noteform, 'amount': facture.prix_total()},
"cotisations/payment.html",
request
)
facture.valid = True
facture.save()
messages.success(request, _("The payment with note was done."))
return redirect(reverse(
'users:profil',
kwargs={'userid': user.id}
))
return form(
{'form': noteform, 'amount': facture.prix_total()},
"cotisations/payment.html",
request
)

View file

@ -19,9 +19,10 @@
# 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.
from django.conf.urls import include, url from django.conf.urls import include, url
from . import comnpay, cheque from . import comnpay, cheque, note_kfet
urlpatterns = [ urlpatterns = [
url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')), url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')),
url(r'^cheque/', include(cheque.urls, namespace='cheque')), url(r'^cheque/', include(cheque.urls, namespace='cheque')),
url(r'^note_kfet/', include(note_kfet.urls, namespace='note_kfet')),
] ]

View file

@ -49,9 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ article.available_for_everyone | tick }}</td> <td>{{ article.available_for_everyone | tick }}</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 %}"> {% include 'buttons/edit.html' with href='cotisations:edit-article' id=article.id %}
<i class="fa fa-edit"></i>
</a>
{% acl_end %} {% acl_end %}
{% history_button article %} {% history_button article %}
</td> </td>

View file

@ -0,0 +1,101 @@
{% 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 acl %}
{% load logs_extra %}
{% load design %}
<div class="table-responsive">
{% if cost_estimate_list.paginator %}
{% include 'pagination.html' with list=cost_estimate_list%}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>
{% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_recip %}
</th>
<th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
<th>
{% trans "Payment method" as tr_payment_method %}
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
</th>
<th>
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
</th>
<th>
{% trans "Validity" as tr_validity %}
{% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %}
</th>
<th>
{% trans "Cost estimate ID" as tr_estimate_id %}
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %}
</th>
<th>
{% trans "Invoice created" as tr_invoice_created%}
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %}
</th>
<th></th>
<th></th>
</tr>
</thead>
{% for estimate in cost_estimate_list %}
<tr>
<td>{{ estimate.recipient }}</td>
<td>{{ estimate.name }}</td>
<td>{{ estimate.prix_total }}</td>
<td>{{ estimate.payment }}</td>
<td>{{ estimate.date }}</td>
<td>{{ estimate.validity }}</td>
<td>{{ estimate.id }}</td>
<td>
{% if estimate.final_invoice %}
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
{% else %}
<i style="color: #D10115;" class="fa fa-times"></i>
{% endif %}
</td>
<td>
{% can_edit estimate %}
{% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %}
{% acl_end %}
{% history_button estimate %}
{% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-to-invoice' estimate.id %}">
<i class="fa fa-file"></i>
</a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-pdf' estimate.id %}">
<i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
</a>
</td>
</tr>
{% endfor %}
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -78,11 +78,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
{% if facture.valid %} {% if facture.valid %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:facture-pdf' facture.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:facture-pdf' facture.id %}">
<i class="fa fa-file-pdf"></i> {% trans "PDF" %} <i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
</a> </a>
{% else %} {% else %}
<i class="text-danger">{% trans "Invalidated invoice" %}</i> <i class="text-danger">{% trans "Invalidated invoice" %}</i>
{% endif %} {% endif %}
{% if facture.control and facture.is_subscription %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:voucher-pdf' facture.id %}">
<i class="fa fa-file-pdf-o"></i> {% trans "Voucher" %}
</a>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th> <th>
{% trans "Recipient" as tr_recip %} {% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %} {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_recip %}
</th> </th>
<th>{% trans "Designation" %}</th> <th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th> <th>{% trans "Total price" %}</th>
@ -76,7 +76,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %} {% acl_end %}
{% history_button invoice %} {% history_button invoice %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:custom-invoice-pdf' invoice.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:custom-invoice-pdf' invoice.id %}">
<i class="fa fa-file-pdf"></i> {% trans "PDF" %} <i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
</a> </a>
</td> </td>
</tr> </tr>

View file

@ -45,9 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td> </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 %}"> {% include 'buttons/edit.html' with href='cotisations:edit-paiement' id=paiement.id %}
<i class="fa fa-edit"></i>
</a>
{% acl_end %} {% acl_end %}
{% history_button paiement %} {% history_button paiement %}
</td> </td>

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -105,7 +105,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
{% trans "Edit" as tr_edit %} {% trans "Edit" as tr_edit %}
{% bootstrap_button tr_edit button_type='submit' icon='star' %} {% bootstrap_button tr_edit button_type='submit' icon='ok' button_class='btn-success' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %} {% 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
@ -36,7 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% blocktrans %}Warning: are you sure you really want to delete this {{ object_name }} object ( {{ objet }} )?{% endblocktrans %} {% blocktrans %}Warning: are you sure you really want to delete this {{ object_name }} object ( {{ objet }} )?{% endblocktrans %}
</h4> </h4>
{% trans "Confirm" as tr_confirm %} {% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='trash' %} {% bootstrap_button tr_confirm button_type='submit' icon='trash' button_class='btn-danger' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -35,10 +35,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h3>{% trans "Edit the invoice" %}</h3> {% if title %}
<h3>{{title}}</h3>
{% else %}
<h3>{% trans "Edit invoice" %}</h3>
{% endif %}
{% massive_bootstrap_form factureform 'user' %} {% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }} {{ venteform.management_form }}
<h3>{% trans "Invoice's articles" %}</h3> <h3>{% trans "Articles" %}</h3>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -58,7 +62,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
{% trans "Confirm" as tr_confirm %} {% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='star' %} {% bootstrap_button tr_confirm button_type='submit' icon='ok' button_class='btn-success' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,22 @@
Bonjour {{name}} !
Nous vous informons que votre cotisation auprès de {{asso_name}} a été acceptée. Vous voilà donc membre de l'association.
Vous trouverez en pièce jointe un reçu.
Pour nous faire part de toute remarque, suggestion ou problème vous pouvez nous envoyer un mail à {{asso_email}}.
À bientôt,
L'équipe de {{asso_name}}.
---
Your subscription to {{asso_name}} has just been accepted. You are now a full member of {{asso_name}}.
You will find with this email a subscription voucher.
For any information, suggestion or problem, you can contact us via email at
{{asso_email}}.
Regards,
The {{asso_name}} team.

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if title %} {% if title %}
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
{% else %} {% else %}
<h3>{% trans "New invoice" %}</h3> <h3>{% trans "Buy" %}</h3>
{% endif %} {% endif %}
{% if max_balance %} {% if max_balance %}
<h4>{% blocktrans %}Maximum allowed balance: {{ max_balance }} €{% endblocktrans %}</h4> <h4>{% blocktrans %}Maximum allowed balance: {{ max_balance }} €{% endblocktrans %}</h4>
@ -44,6 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %} {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{% if factureform %}
{% bootstrap_form_errors factureform %}
{% endif %}
{% if discount_form %}
{% bootstrap_form_errors discount_form %}
{% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
@ -53,7 +59,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div id="paymentMethod"></div> <div id="paymentMethod"></div>
{% endif %} {% endif %}
{% if articlesformset %} {% if articlesformset %}
<h3>{% trans "Invoice's articles" %}</h3> <h3>{% trans "Articles" %}</h3>
<div id="form_set" class="form-group"> <div id="form_set" class="form-group">
{{ articlesformset.management_form }} {{ articlesformset.management_form }}
{% for articlesform in articlesformset.forms %} {% for articlesform in articlesformset.forms %}
@ -67,12 +73,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one"> <input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
{% if discount_form %}
<h3>{% trans "Discount" %}</h3>
{% bootstrap_form discount_form %}
{% endif %}
<p> <p>
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %} {% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{% bootstrap_button action_name button_type='submit' icon='star' %} {% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
</form> </form>
{% if articlesformset or payment_method%} {% if articlesformset or payment_method%}
@ -119,6 +129,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
'id_form-' + i.toString() + '-quantity').value; 'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity; price += article_price * quantity;
} }
{% if discount_form %}
var relative_discount = document.getElementById('id_is_relative').checked;
var discount = document.getElementById('id_discount').value;
if(relative_discount) {
discount = discount/100 * price;
}
price -= discount;
{% endif %}
document.getElementById('total_price').innerHTML = document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ','); price.toFixed(2).toString().replace('.', ',');
} }
@ -148,6 +166,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
for (i = 0; i < product_count; ++i){ for (i = 0; i < product_count; ++i){
add_listenner_for_id(i); add_listenner_for_id(i);
} }
document.getElementById('id_discount')
.addEventListener('change', update_price, true);
document.getElementById('id_is_relative')
.addEventListener('click', update_price, true);
update_price(); update_price();
}); });
{% endif %} {% endif %}

View file

@ -75,8 +75,12 @@
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
{% if fid is not None %} {% if fid is not None %}
{% if is_estimate %}
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
{% else %}
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\ {\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
{% endif %} {% endif %}
{% endif %}
\end{tabular*} \end{tabular*}
\\ \\
@ -104,12 +108,30 @@
\begin{tabular}{|l|r|} \begin{tabular}{|l|r|}
\hline \hline
\textbf{Total} & {{total|floatformat:2}} \euro \\ \textbf{Total} & {{total|floatformat:2}} \euro \\
{% if not is_estimate %}
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\ \textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
\doublehline \doublehline
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
{% endif %}
\hline \hline
\end{tabular} \end{tabular}
\vspace{1cm}
\begin{tabularx}{\textwidth}{r X}
\hline
\textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\
\hline
{% if remark %}
\textbf{Remarque} & {{remark|safe}} \\
\hline
{% endif %}
{% if end_validity %}
\textbf{Validité} & Jusqu'au {{end_validity}} \\
\hline
{% endif %}
\end{tabularx}
\vfill \vfill

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Articles" %}{% endblock %} {% block title %}{% trans "Articles" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Article types list" %}</h2> <h2>{% trans "List of article types" %}</h2>
{% can_create Article %} {% can_create Article %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-article' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-article' %}">
<i class="fa fa-cart-plus"></i> {% trans "Add an article type" %} <i class="fa fa-cart-plus"></i> {% trans "Add an article type" %}

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Banks" %}{% endblock %} {% block title %}{% trans "Banks" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Banks list" %}</h2> <h2>{% trans "List of banks" %}</h2>
{% can_create Banque %} {% can_create Banque %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-banque' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-banque' %}">
<i class="fa fa-cart-plus"></i> {% trans "Add a bank" %} <i class="fa fa-cart-plus"></i> {% trans "Add a bank" %}

View file

@ -0,0 +1,36 @@
{% 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 acl %}
{% load i18n %}
{% block title %}{% trans "Cost estimates" %}{% endblock %}
{% block content %}
<h2>{% trans "List of cost estimates" %}</h2>
{% can_create CostEstimate %}
{% include 'buttons/add.html' with href='cotisations:new-cost-estimate'%}
{% acl_end %}
{% include 'cotisations/aff_cost_estimate.html' %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -28,9 +28,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Custom invoices" %}{% endblock %} {% block title %}{% trans "Custom invoices" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Custom invoices list" %}</h2> <h2>{% trans "List of custom invoices" %}</h2>
{% can_create CustomInvoice %} {% can_create CustomInvoice %}
{% include "buttons/add.html" with href='cotisations:new-custom-invoice'%} {% include 'buttons/add.html' with href='cotisations:new-custom-invoice'%}
{% acl_end %} {% acl_end %}
{% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %} {% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %}
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %} {% 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
@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_form form %} {% bootstrap_form form %}
{% endif %} {% 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' button_class='btn-success' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends 'base.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
@ -28,35 +28,40 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block sidebar %} {% block sidebar %}
{% can_create CustomInvoice %} {% can_create CustomInvoice %}
<a class="list-group-item list-group-item-success" href="{% url "cotisations:new-custom-invoice" %}"> <a class="list-group-item list-group-item-success" href="{% url 'cotisations:new-custom-invoice' %}">
<i class="fa fa-plus"></i> {% trans "Create an invoice" %} <i class="fa fa-plus"></i> {% trans "Create an invoice" %}
</a> </a>
<a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}"> <a class="list-group-item list-group-item-warning" href="{% url 'cotisations:control' %}">
<i class="fa fa-eye"></i> {% trans "Control the invoices" %} <i class="fa fa-eye"></i> {% trans "Control the invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Facture %} {% can_view_all Facture %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index" %}"> <a class="list-group-item list-group-item-info" href="{% url 'cotisations:index' %}">
<i class="fa fa-list-ul"></i> {% trans "Invoices" %} <i class="fa fa-list-ul"></i> {% trans "Invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all CustomInvoice %} {% can_view_all CustomInvoice %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-custom-invoice" %}"> <a class="list-group-item list-group-item-info" href="{% url 'cotisations:index-custom-invoice' %}">
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %} <i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all CostEstimate %}
<a class="list-group-item list-group-item-info" href="{% url 'cotisations:index-cost-estimate' %}">
<i class="fa fa-list-ul"></i> {% trans "Cost estimates" %}
</a>
{% acl_end %}
{% can_view_all Article %} {% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}"> <a class="list-group-item list-group-item-info" href="{% url 'cotisations:index-article' %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %} <i class="fa fa-list-ul"></i> {% trans "Available articles" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Banque %} {% can_view_all Banque %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-banque" %}"> <a class="list-group-item list-group-item-info" href="{% url 'cotisations:index-banque' %}">
<i class="fa fa-list-ul"></i> {% trans "Banks" %} <i class="fa fa-list-ul"></i> {% trans "Banks" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Paiement %} {% can_view_all Paiement %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-paiement" %}"> <a class="list-group-item list-group-item-info" href="{% url 'cotisations:index-paiement' %}">
<i class="fa fa-list-ul"></i> {% trans "Payment methods" %} <i class="fa fa-list-ul"></i> {% trans "Payment methods" %}
</a> </a>
{% acl_end %} {% acl_end %}

View file

@ -0,0 +1,87 @@
{% load i18n %}
{% language 'fr' %}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Invoice Template
% LaTeX Template
% Version 1.0 (3/11/12)
%% This template has been downloaded from:
% http://www.LaTeXTemplates.com
%
% Original author:
% Trey Hunner (http://www.treyhunner.com/)
%
% License:
% CC BY-NC-SA 3.0 (http://creativecommons.org/licenses/by-nc-sa/3.0/)
%
% Important note:
% This template requires the invoice.cls file to be in the same directory as
% the .tex file. The invoice.cls file provides the style used for structuring the
% document.
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%----------------------------------------------------------------------------------------
% DOCUMENT CONFIGURATION
%----------------------------------------------------------------------------------------
\documentclass[12pt]{article} % Use the custom invoice class (invoice.cls)
\usepackage[utf8]{inputenc}
\usepackage[letterpaper,hmargin=0.79in,vmargin=0.79in]{geometry}
\usepackage{longtable}
\usepackage{graphicx}
\usepackage{tabularx}
\usepackage{eurosym}
\usepackage{multicol}
\pagestyle{empty} % No page numbers
\linespread{1.5}
\newcommand{\doublehline}{\noalign{\hrule height 1pt}}
\setlength{\parindent}{0cm}
\begin{document}
%----------------------------------------------------------------------------------------
% HEADING SECTION
%----------------------------------------------------------------------------------------
\begin{center}
{\Huge\bf Reçu d'adhésion \\ {{asso_name|safe}} } % Company providing the invoice
\end{center}
\bigskip
\hrule
\bigskip
\vfill
Je sousigné, {{pres_name|safe}}, déclare par la présente avoir reçu le bulletin d'adhésion de:
\begin{center}
\setlength{\tabcolsep}{10pt} % Make table columns tighter, usefull for postionning
\begin{tabular}{r l r l}
{\bf Prénom :}~ & {{firstname|safe}} & {% if phone %}{\bf Téléphone :}~ & {{phone}}{% else %} & {% endif %} \\
{\bf Nom :}~ & {{lastname|safe}} & {\bf Mail :}~ & {{email|safe}} \\
\end{tabular}
\end{center}
\bigskip
ainsi que sa cotisation.
Le postulant, déclare reconnaître l'objet de l'association, et en a accepté les statuts ainsi que le règlement intérieur qui sont mis à sa disposition dans les locaux de l'association. L'adhésion du membre sus-nommé est ainsi validée. Ce reçu confirme la qualité de membre du postulant, et ouvre droit à la participation à l'assemblée générale de l'association jusqu'au {{date_end|date:"d F Y"}}.
\bigskip
Validé électroniquement par {{pres_name|safe}}, le {{date_begin|date:"d/m/Y"}}.
\vfill
\hrule
\smallskip
\footnotesize
Les informations recueillies sont nécessaires pour votre adhésion. Conformément à la loi "Informatique et Libertés" du 6 janvier 1978, vous disposez d'un droit d'accès et de rectification aux données personnelles vous concernant. Pour l'exercer, adressez-vous au secrétariat de l'association.
\end{document}
{% endlanguage %}

View file

@ -1,4 +1,4 @@
# coding: utf-8 # -*- 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.
@ -31,11 +31,16 @@ from subprocess import Popen, PIPE
import os import os
from datetime import datetime from datetime import datetime
from django.db import models
from django.template.loader import get_template from django.template.loader import get_template
from django.template import Context from django.template import Context
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from re2o.mixins import AclMixin, RevMixin
from preferences.models import CotisationsOption
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
@ -48,15 +53,40 @@ def render_invoice(_request, ctx={}):
Render an invoice using some available information such as the current Render an invoice using some available information such as the current
date, the user, the articles, the prices, ... date, the user, the articles, the prices, ...
""" """
options, _ = CotisationsOption.objects.get_or_create()
is_estimate = ctx.get('is_estimate', False)
filename = '_'.join([ filename = '_'.join([
'invoice', 'cost_estimate' if is_estimate else 'invoice',
slugify(ctx.get('asso_name', "")), slugify(ctx.get('asso_name', "")),
slugify(ctx.get('recipient_name', "")), slugify(ctx.get('recipient_name', "")),
str(ctx.get('DATE', datetime.now()).year), str(ctx.get('DATE', datetime.now()).year),
str(ctx.get('DATE', datetime.now()).month), str(ctx.get('DATE', datetime.now()).month),
str(ctx.get('DATE', datetime.now()).day), str(ctx.get('DATE', datetime.now()).day),
]) ])
r = render_tex(_request, 'cotisations/factures.tex', ctx) templatename = options.invoice_template.template.name.split('/')[-1]
r = render_tex(_request, templatename, ctx)
r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format(
name=filename
)
return r
def render_voucher(_request, ctx={}):
"""
Render a subscribtion voucher.
"""
options, _ = CotisationsOption.objects.get_or_create()
filename = '_'.join([
'voucher',
slugify(ctx.get('asso_name', "")),
slugify(ctx.get('firstname', "")),
slugify(ctx.get('lastname', "")),
str(ctx.get('date_begin', datetime.now()).year),
str(ctx.get('date_begin', datetime.now()).month),
str(ctx.get('date_begin', datetime.now()).day),
])
templatename = options.voucher_template.template.name.split('/')[-1]
r = render_tex(_request, templatename, ctx)
r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format(
name=filename name=filename
) )
@ -81,10 +111,11 @@ def create_pdf(template, ctx={}):
with tempfile.TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory() as tempdir:
for _ in range(2): for _ in range(2):
with open("/var/www/re2o/out.log", "w") as f:
process = Popen( process = Popen(
['pdflatex', '-output-directory', tempdir], ['pdflatex', '-output-directory', tempdir],
stdin=PIPE, stdin=PIPE,
stdout=PIPE, stdout=f,#PIPE,
) )
process.communicate(rendered_tpl) process.communicate(rendered_tpl)
with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f: with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f:
@ -93,6 +124,20 @@ def create_pdf(template, ctx={}):
return pdf return pdf
def escape_chars(string):
"""Escape the '%' and the '' signs to avoid messing with LaTeX"""
if not isinstance(string, str):
return string
mapping = (
('', r'\euro'),
('%', r'\%'),
)
r = str(string)
for k, v in mapping:
r = r.replace(k, v)
return r
def render_tex(_request, template, ctx={}): def render_tex(_request, template, ctx={}):
"""Creates a PDF from a LaTex templates using pdflatex. """Creates a PDF from a LaTex templates using pdflatex.

View file

@ -51,11 +51,46 @@ urlpatterns = [
views.facture_pdf, views.facture_pdf,
name='facture-pdf' name='facture-pdf'
), ),
url(
r'^voucher_pdf/(?P<factureid>[0-9]+)$',
views.voucher_pdf,
name='voucher-pdf'
),
url(
r'^new_cost_estimate/$',
views.new_cost_estimate,
name='new-cost-estimate'
),
url(
r'^index_cost_estimate/$',
views.index_cost_estimate,
name='index-cost-estimate'
),
url(
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_pdf,
name='cost-estimate-pdf',
),
url( url(
r'^index_custom_invoice/$', r'^index_custom_invoice/$',
views.index_custom_invoice, views.index_custom_invoice,
name='index-custom-invoice' name='index-custom-invoice'
), ),
url(
r'^edit_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.edit_cost_estimate,
name='edit-cost-estimate'
),
url(
r'^cost_estimate_to_invoice/(?P<costestimateid>[0-9]+)$',
views.cost_estimate_to_invoice,
name='cost-estimate-to-invoice'
),
url(
r'^del_cost_estimate/(?P<costestimateid>[0-9]+)$',
views.del_cost_estimate,
name='del-cost-estimate'
),
url( url(
r'^new_custom_invoice/$', r'^new_custom_invoice/$',
views.new_custom_invoice, views.new_custom_invoice,

View file

@ -25,7 +25,7 @@ from django.template.loader import get_template
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from .tex import create_pdf from .tex import create_pdf
from preferences.models import AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption, CotisationsOption
from re2o.settings import LOGO_PATH from re2o.settings import LOGO_PATH
from re2o import settings from re2o import settings
@ -89,7 +89,42 @@ def send_mail_invoice(invoice):
'Votre facture / Your invoice', 'Votre facture / Your invoice',
template.render(ctx), template.render(ctx),
GeneralOption.get_cached_value('email_from'), GeneralOption.get_cached_value('email_from'),
[invoice.user.email], [invoice.user.get_mail],
attachments=[('invoice.pdf', pdf, 'application/pdf')] attachments=[('invoice.pdf', pdf, 'application/pdf')]
) )
mail.send() mail.send()
def send_mail_voucher(invoice):
"""Creates a voucher from an invoice and sends it by email to the client"""
ctx = {
'asso_name': AssoOption.get_cached_value('name'),
'pres_name': AssoOption.get_cached_value('pres_name'),
'firstname': invoice.user.name,
'lastname': invoice.user.surname,
'email': invoice.user.email,
'phone': invoice.user.telephone,
'date_end': invoice.get_subscription().latest('date_end').date_end,
'date_begin': invoice.get_subscription().earliest('date_start').date_start
}
templatename = CotisationsOption.get_cached_value('voucher_template').template.name.split('/')[-1]
pdf = create_pdf(templatename, ctx)
template = get_template('cotisations/email_subscription_accepted')
ctx = {
'name': "{} {}".format(
invoice.user.name,
invoice.user.surname
),
'asso_email': AssoOption.get_cached_value('contact'),
'asso_name': AssoOption.get_cached_value('name')
}
mail = EmailMessage(
'Votre reçu / Your voucher',
template.render(ctx),
GeneralOption.get_cached_value('email_from'),
[invoice.user.get_mail],
attachments=[('voucher.pdf', pdf, 'application/pdf')]
)
mail.send()

View file

@ -47,7 +47,10 @@ from users.models import User
from re2o.settings import LOGO_PATH from re2o.settings import LOGO_PATH
from re2o import settings from re2o import settings
from re2o.views import form from re2o.views import form
from re2o.utils import SortTable, re2o_paginator from re2o.base import (
SortTable,
re2o_paginator,
)
from re2o.acl import ( from re2o.acl import (
can_create, can_create,
can_edit, can_edit,
@ -65,7 +68,8 @@ from .models import (
Paiement, Paiement,
Banque, Banque,
CustomInvoice, CustomInvoice,
BaseInvoice BaseInvoice,
CostEstimate,
) )
from .forms import ( from .forms import (
FactureForm, FactureForm,
@ -77,11 +81,13 @@ from .forms import (
DelBanqueForm, DelBanqueForm,
SelectArticleForm, SelectArticleForm,
RechargeForm, RechargeForm,
CustomInvoiceForm CustomInvoiceForm,
DiscountForm,
CostEstimateForm,
) )
from .tex import render_invoice from .tex import render_invoice, render_voucher, escape_chars
from .payment_methods.forms import payment_method_factory from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method, send_mail_invoice from .utils import find_payment_method
@login_required @login_required
@ -148,8 +154,6 @@ def new_facture(request, user, userid):
p.facture = new_invoice_instance p.facture = new_invoice_instance
p.save() p.save()
send_mail_invoice(new_invoice_instance)
return new_invoice_instance.paiement.end_payment( return new_invoice_instance.paiement.end_payment(
new_invoice_instance, new_invoice_instance,
request request
@ -171,13 +175,65 @@ def new_facture(request, user, userid):
'articlesformset': article_formset, 'articlesformset': article_formset,
'articlelist': article_list, 'articlelist': article_list,
'balance': balance, 'balance': balance,
'action_name': _('Create'), 'action_name': _('Confirm'),
}, },
'cotisations/facture.html', request 'cotisations/facture.html', request
) )
# TODO : change facture to invoice @login_required
@can_create(CostEstimate)
def new_cost_estimate(request):
"""
View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative
point of view.
"""
# The template needs the list of articles (for the JS part)
articles = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name)
)
# Building the invocie form and the article formset
cost_estimate_form = CostEstimateForm(request.POST or None)
articles_formset = formset_factory(SelectArticleForm)(
request.POST or None,
form_kwargs={'user': request.user}
)
discount_form = DiscountForm(request.POST or None)
if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
cost_estimate_instance = cost_estimate_form.save()
for art_item in articles_formset:
if art_item.cleaned_data:
article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity']
Vente.objects.create(
facture=cost_estimate_instance,
name=article.name,
prix=article.prix,
type_cotisation=article.type_cotisation,
duration=article.duration,
number=quantity
)
discount_form.apply_to_invoice(cost_estimate_instance)
messages.success(
request,
_("The cost estimate was created.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': cost_estimate_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles,
'discount_form': discount_form,
'title': _("Cost estimate"),
}, 'cotisations/facture.html', request)
@login_required @login_required
@can_create(CustomInvoice) @can_create(CustomInvoice)
def new_custom_invoice(request): def new_custom_invoice(request):
@ -193,12 +249,13 @@ def new_custom_invoice(request):
# Building the invocie form and the article formset # Building the invocie form and the article formset
invoice_form = CustomInvoiceForm(request.POST or None) invoice_form = CustomInvoiceForm(request.POST or None)
article_formset = formset_factory(SelectArticleForm)( articles_formset = formset_factory(SelectArticleForm)(
request.POST or None, request.POST or None,
form_kwargs={'user': request.user, 'target_user': user} form_kwargs={'user': request.user}
) )
discount_form = DiscountForm(request.POST or None)
if invoice_form.is_valid() and articles_formset.is_valid(): if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
new_invoice_instance = invoice_form.save() new_invoice_instance = invoice_form.save()
for art_item in articles_formset: for art_item in articles_formset:
if art_item.cleaned_data: if art_item.cleaned_data:
@ -212,18 +269,19 @@ def new_custom_invoice(request):
duration=article.duration, duration=article.duration,
number=quantity number=quantity
) )
discount_form.apply_to_invoice(new_invoice_instance)
messages.success( messages.success(
request, request,
_("The custom invoice was created.") _("The custom invoice was created.")
) )
return redirect(reverse('cotisations:index-custom-invoice')) return redirect(reverse('cotisations:index-custom-invoice'))
return form({ return form({
'factureform': invoice_form, 'factureform': invoice_form,
'action_name': _("Create"), 'action_name': _("Confirm"),
'articlesformset': articles_formset, 'articlesformset': articles_formset,
'articlelist': articles 'articlelist': articles,
'discount_form': discount_form
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -266,7 +324,8 @@ def facture_pdf(request, facture, **_kwargs):
'siret': AssoOption.get_cached_value('siret'), 'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'), 'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'), 'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': facture.paiement.moyen,
}) })
@ -331,6 +390,55 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request) }, 'cotisations/delete.html', request)
@login_required
@can_edit(CostEstimate)
def edit_cost_estimate(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CostEstimateForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(purchases_objects)
)
purchase_form = purchase_form_set(
request.POST or None,
queryset=purchases_objects
)
if invoice_form.is_valid() and purchase_form.is_valid():
if invoice_form.changed_data:
invoice_form.save()
purchase_form.save()
messages.success(
request,
_("The cost estimate was edited.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'factureform': invoice_form,
'venteform': purchase_form,
'title': _("Edit cost estimate")
}, 'cotisations/edit_facture.html', request)
@login_required
@can_edit(CostEstimate)
@can_create(CustomInvoice)
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
"""Create a custom invoice from a cos estimate"""
cost_estimate.create_invoice()
messages.success(
request,
_("An invoice was successfully created from your cost estimate.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
@login_required @login_required
@can_edit(CustomInvoice) @can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs): def edit_custom_invoice(request, invoice, **kwargs):
@ -367,22 +475,21 @@ def edit_custom_invoice(request, invoice, **kwargs):
@login_required @login_required
@can_view(CustomInvoice) @can_view(CostEstimate)
def custom_invoice_pdf(request, invoice, **_kwargs): def cost_estimate_pdf(request, invoice, **_kwargs):
""" """
View used to generate a PDF file from an existing invoice in database View used to generate a PDF file from an existing cost estimate in database
Creates a line for each Purchase (thus article sold) and generate the Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the invoice with the total price, the payment method, the address and the
legal information for the user. legal information for the user.
""" """
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice) purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it # Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price) # contiaining (article_name, article_price, quantity, total_price)
purchases_info = [] purchases_info = []
for purchase in purchases_objects: for purchase in purchases_objects:
purchases_info.append({ purchases_info.append({
'name': purchase.name, 'name': escape_chars(purchase.name),
'price': purchase.prix, 'price': purchase.prix,
'quantity': purchase.number, 'quantity': purchase.number,
'total_price': purchase.prix_total 'total_price': purchase.prix_total
@ -401,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
'siret': AssoOption.get_cached_value('siret'), 'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'), 'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'), 'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
'end_validity': invoice.date + invoice.validity,
'is_estimate': True,
})
@login_required
@can_delete(CostEstimate)
def del_cost_estimate(request, estimate, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
estimate.delete()
messages.success(
request,
_("The cost estimate was deleted.")
)
return redirect(reverse('cotisations:index-cost-estimate'))
return form({
'objet': estimate,
'objet_name': _("Cost estimate")
}, 'cotisations/delete.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing invoice in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
'payment_method': invoice.payment,
'remark': invoice.remark,
}) })
# TODO : change facture to invoice
@login_required @login_required
@can_delete(CustomInvoice) @can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs): def del_custom_invoice(request, invoice, **_kwargs):
@ -663,8 +833,8 @@ def del_banque(request, instances):
except ProtectedError: except ProtectedError:
messages.error( messages.error(
request, request,
_("The bank %(bank_name)s can't be deleted \ _("The bank %(bank_name)s can't be deleted because there"
because there are invoices using it.") % { " are invoices using it.") % {
'bank_name': bank_del 'bank_name': bank_del
} }
) )
@ -756,12 +926,36 @@ def index_banque(request):
}) })
@login_required
@can_view_all(CustomInvoice)
def index_cost_estimate(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
cost_estimate_list = SortTable.sort(
cost_estimate_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
cost_estimate_list = re2o_paginator(
request,
cost_estimate_list,
pagination_number,
)
return render(request, 'cotisations/index_cost_estimate.html', {
'cost_estimate_list': cost_estimate_list
})
@login_required @login_required
@can_view_all(CustomInvoice) @can_view_all(CustomInvoice)
def index_custom_invoice(request): def index_custom_invoice(request):
"""View used to display every custom invoice.""" """View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number') pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set') cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
custom_invoice_list = CustomInvoice.objects.prefetch_related(
'vente_set').exclude(id__in=cost_estimate_ids)
custom_invoice_list = SortTable.sort( custom_invoice_list = SortTable.sort(
custom_invoice_list, custom_invoice_list,
request.GET.get('col'), request.GET.get('col'),
@ -827,7 +1021,8 @@ def credit_solde(request, user, **_kwargs):
kwargs={'userid': user.id} kwargs={'userid': user.id}
)) ))
refill_form = RechargeForm(request.POST or None, user=user, user_source=request.user) refill_form = RechargeForm(
request.POST or None, user=user, user_source=request.user)
if refill_form.is_valid(): if refill_form.is_valid():
price = refill_form.cleaned_data['value'] price = refill_form.cleaned_data['value']
invoice = Facture(user=user) invoice = Facture(user=user)
@ -839,7 +1034,6 @@ def credit_solde(request, user, **_kwargs):
else: else:
price_ok = True price_ok = True
if price_ok: if price_ok:
invoice.valid = True
invoice.save() invoice.save()
Vente.objects.create( Vente.objects.create(
facture=invoice, facture=invoice,
@ -848,8 +1042,6 @@ def credit_solde(request, user, **_kwargs):
number=1 number=1
) )
send_mail_invoice(invoice)
return invoice.paiement.end_payment(invoice, request) return invoice.paiement.end_payment(invoice, request)
p = get_object_or_404(Paiement, is_balance=True) p = get_object_or_404(Paiement, is_balance=True)
return form({ return form({
@ -857,6 +1049,32 @@ def credit_solde(request, user, **_kwargs):
'balance': user.solde, 'balance': user.solde,
'title': _("Refill your balance"), 'title': _("Refill your balance"),
'action_name': _("Pay"), 'action_name': _("Pay"),
'max_balance': p.payment_method.maximum_balance, 'max_balance': find_payment_method(p).maximum_balance,
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@login_required
@can_view(Facture)
def voucher_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from a controlled invoice
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
if not invoice.control:
messages.error(
request,
_("Could not find a voucher for that invoice.")
)
return redirect(reverse('cotisations:index'))
return render_voucher(request, {
'asso_name': AssoOption.get_cached_value('name'),
'pres_name': AssoOption.get_cached_value('pres_name'),
'firstname': invoice.user.name,
'lastname': invoice.user.surname,
'email': invoice.user.email,
'phone': invoice.user.telephone,
'date_end': invoice.get_subscription().latest('date_end').date_end,
'date_begin': invoice.date
})

View file

@ -38,6 +38,7 @@ Inspiré du travail de Daniel Stan au Crans
import os import os
import sys import sys
import logging import logging
import traceback
import radiusd # Module magique freeradius (radiusd.py is dummy) import radiusd # Module magique freeradius (radiusd.py is dummy)
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
@ -57,14 +58,9 @@ application = get_wsgi_application()
from machines.models import Interface, IpList, Nas, Domain from machines.models import Interface, IpList, Nas, Domain
from topologie.models import Port, Switch from topologie.models import Port, Switch
from users.models import User from users.models import User
from preferences.models import OptionalTopologie from preferences.models import RadiusOption
options, created = OptionalTopologie.objects.get_or_create()
VLAN_NOK = options.vlan_decision_nok.vlan_id
VLAN_OK = options.vlan_decision_ok.vlan_id
RADIUS_POLICY = options.radius_general_policy
#: Serveur radius de test (pas la prod) #: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
@ -81,7 +77,7 @@ class RadiusdHandler(logging.Handler):
rad_sig = radiusd.L_INFO rad_sig = radiusd.L_INFO
else: else:
rad_sig = radiusd.L_DBG rad_sig = radiusd.L_DBG
radiusd.radlog(rad_sig, record.msg) radiusd.radlog(rad_sig, record.msg.encode('utf-8'))
# Initialisation d'un logger (pour logguer unifié) # Initialisation d'un logger (pour logguer unifié)
@ -122,7 +118,8 @@ def radius_event(fun):
return fun(data) return fun(data)
except Exception as err: except Exception as err:
logger.error('Failed %r on data %r' % (err, auth_data)) logger.error('Failed %r on data %r' % (err, auth_data))
raise logger.debug('Function %r, Traceback: %s' % (fun, repr(traceback.format_stack())))
return radiusd.RLM_MODULE_FAIL
return new_f return new_f
@ -194,12 +191,12 @@ def post_auth(data):
nas_instance = find_nas_from_request(nas) nas_instance = find_nas_from_request(nas)
# Toutes les reuquètes non proxifiées # Toutes les reuquètes non proxifiées
if not nas_instance: if not nas_instance:
logger.info(u"Requète proxifiée, nas inconnu".encode('utf-8')) logger.info(u"Requete proxifiee, nas inconnu".encode('utf-8'))
return radiusd.RLM_MODULE_OK return radiusd.RLM_MODULE_OK
nas_type = Nas.objects.filter(nas_type=nas_instance.type).first() nas_type = Nas.objects.filter(nas_type=nas_instance.type).first()
if not nas_type: if not nas_type:
logger.info( logger.info(
u"Type de nas non enregistré dans la bdd!".encode('utf-8') u"Type de nas non enregistre dans la bdd!".encode('utf-8')
) )
return radiusd.RLM_MODULE_OK return radiusd.RLM_MODULE_OK
@ -227,9 +224,10 @@ def post_auth(data):
# On récupère le numéro du port sur l'output de freeradius. # On récupère le numéro du port sur l'output de freeradius.
# La ligne suivante fonctionne pour cisco, HP et Juniper # La ligne suivante fonctionne pour cisco, HP et Juniper
port = port.split(".")[0].split('/')[-1][-2:] port = port.split(".")[0].split('/')[-1][-2:]
out = decide_vlan_and_register_switch(nas_machine, nas_type, port, mac) out = decide_vlan_switch(nas_machine, nas_type, port, mac)
sw_name, room, reason, vlan_id = out sw_name, room, reason, vlan_id, decision = out
if decision:
log_message = '(fil) %s -> %s [%s%s]' % ( log_message = '(fil) %s -> %s [%s%s]' % (
sw_name + u":" + port + u"/" + str(room), sw_name + u":" + port + u"/" + str(room),
mac, mac,
@ -248,6 +246,15 @@ def post_auth(data):
), ),
() ()
) )
else:
log_message = '(fil) %s -> %s [Reject:%s]' % (
sw_name + u":" + port + u"/" + str(room),
mac,
(reason and u': ' + reason).encode('utf-8')
)
logger.info(log_message)
return radiusd.RLM_MODULE_REJECT
else: else:
return radiusd.RLM_MODULE_OK return radiusd.RLM_MODULE_OK
@ -284,19 +291,19 @@ def check_user_machine_and_register(nas_type, username, mac_address):
Renvoie le mot de passe ntlm de l'user si tout est ok Renvoie le mot de passe ntlm de l'user si tout est ok
Utilise pour les authentifications en 802.1X""" Utilise pour les authentifications en 802.1X"""
interface = Interface.objects.filter(mac_address=mac_address).first() interface = Interface.objects.filter(mac_address=mac_address).first()
user = User.objects.filter(pseudo=username).first() user = User.objects.filter(pseudo__iexact=username).first()
if not user: if not user:
return (False, u"User inconnu", '') return (False, u"User inconnu", '')
if not user.has_access(): if not user.has_access():
return (False, u"Adhérent non cotisant", '') return (False, u"Adherent non cotisant", '')
if interface: if interface:
if interface.machine.user != user: if interface.machine.user != user:
return (False, return (False,
u"Machine enregistrée sur le compte d'un autre " u"Machine enregistree sur le compte d'un autre "
"user...", "user...",
'') '')
elif not interface.is_active: elif not interface.is_active:
return (False, u"Machine desactivée", '') return (False, u"Machine desactivee", '')
elif not interface.ipv4: elif not interface.ipv4:
interface.assign_ipv4() interface.assign_ipv4()
return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm) return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm)
@ -317,36 +324,51 @@ def check_user_machine_and_register(nas_type, username, mac_address):
return (False, u"Machine inconnue", '') return (False, u"Machine inconnue", '')
def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, def decide_vlan_switch(nas_machine, nas_type, port_number,
mac_address): mac_address):
"""Fonction de placement vlan pour un switch en radius filaire auth par """Fonction de placement vlan pour un switch en radius filaire auth par
mac. mac.
Plusieurs modes : Plusieurs modes :
- nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK - tous les modes:
- nas inconnu: VLAN_OK
- port inconnu: Politique définie dans RadiusOption
- pas de radius sur le port: VLAN_OK - pas de radius sur le port: VLAN_OK
- bloq : VLAN_NOK
- force: placement sur le vlan indiqué dans la bdd - force: placement sur le vlan indiqué dans la bdd
- mode strict: - mode strict:
- pas de chambre associée : VLAN_NOK - pas de chambre associée: Politique définie
- pas d'utilisateur dans la chambre : VLAN_NOK dans RadiusOption
- cotisation non à jour : VLAN_NOK - pas d'utilisateur dans la chambre : Rejet
(redirection web si disponible)
- utilisateur de la chambre banni ou désactivé : Rejet
(redirection web si disponible)
- utilisateur de la chambre non cotisant et non whiteslist:
Politique définie dans RadiusOption
- sinon passe à common (ci-dessous) - sinon passe à common (ci-dessous)
- mode common : - mode common :
- interface connue (macaddress): - interface connue (macaddress):
- utilisateur proprio non cotisant ou banni : VLAN_NOK - utilisateur proprio non cotisant / machine désactivée:
- user à jour : VLAN_OK Politique définie dans RadiusOption
- utilisateur proprio banni :
Politique définie dans RadiusOption
- user à jour : VLAN_OK (réassignation de l'ipv4 au besoin)
- interface inconnue : - interface inconnue :
- register mac désactivé : VLAN_NOK - register mac désactivé : Politique définie
- register mac activé : dans RadiusOption
- dans la chambre associé au port, pas d'user ou non à - register mac activé: redirection vers webauth
jour : VLAN_NOK Returns:
- user à jour, autocapture de la mac et VLAN_OK tuple avec :
- Nom du switch (str)
- chambre (str)
- raison de la décision (str)
- vlan_id (int)
- decision (bool)
""" """
# Get port from switch and port number # Get port from switch and port number
extra_log = "" extra_log = ""
# Si le NAS est inconnu, on place sur le vlan defaut # Si le NAS est inconnu, on place sur le vlan defaut
if not nas_machine: if not nas_machine:
return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK) return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True)
sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine))) sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
@ -361,7 +383,13 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
# Aucune information particulière ne permet de déterminer quelle # Aucune information particulière ne permet de déterminer quelle
# politique à appliquer sur ce port # politique à appliquer sur ce port
if not port: if not port:
return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK) return (
sw_name,
"Chambre inconnue",
u'Port inconnu',
getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT
)
# On récupère le profil du port # On récupère le profil du port
port_profile = port.get_port_profile port_profile = port.get_port_profile
@ -372,46 +400,82 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
extra_log = u"Force sur vlan " + str(DECISION_VLAN) extra_log = u"Force sur vlan " + str(DECISION_VLAN)
else: else:
DECISION_VLAN = VLAN_OK DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok').vlan_id
# Si le port est désactivé, on rejette sur le vlan de déconnexion # Si le port est désactivé, on rejette la connexion
if not port.state: if not port.state:
return (sw_name, port.room, u'Port desactivé', VLAN_NOK) return (sw_name, port.room, u'Port desactive', None, False)
# Si radius est désactivé, on laisse passer # Si radius est désactivé, on laisse passer
if port_profile.radius_type == 'NO': if port_profile.radius_type == 'NO':
return (sw_name, return (sw_name,
"", "",
u"Pas d'authentification sur ce port" + extra_log, u"Pas d'authentification sur ce port" + extra_log,
DECISION_VLAN) DECISION_VLAN,
True)
# Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment # Si le 802.1X est activé sur ce port, cela veut dire que la personne a
# été accept précédemment
# Par conséquent, on laisse passer sur le bon vlan # Par conséquent, on laisse passer sur le bon vlan
if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X': if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'):
room = port.room or "Chambre/local inconnu" room = port.room or "Chambre/local inconnu"
return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN) return (
sw_name,
room,
u'Acceptation authentification 802.1X',
DECISION_VLAN,
True
)
# Sinon, cela veut dire qu'on fait de l'auth radius par mac # Sinon, cela veut dire qu'on fait de l'auth radius par mac
# Si le port est en mode strict, on vérifie que tous les users # Si le port est en mode strict, on vérifie que tous les users
# rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage) # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette
# Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis # (anti squattage)
# dedans # Il n'est pas possible de se connecter sur une prise strict sans adhérent
# à jour de cotis dedans
if port_profile.radius_mode == 'STRICT': if port_profile.radius_mode == 'STRICT':
room = port.room room = port.room
if not room: if not room:
return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK) return (
sw_name,
"Inconnue",
u'Chambre inconnue',
getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT
)
room_user = User.objects.filter( room_user = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room) Q(club__room=port.room) | Q(adherent__room=port.room)
) )
if not room_user: if not room_user:
return (sw_name, room, u'Chambre non cotisante', VLAN_NOK) return (
sw_name,
room,
u'Chambre non cotisante -> Web redirect',
None,
False
)
for user in room_user: for user in room_user:
if not user.has_access(): if user.is_ban() or user.state != User.STATE_ACTIVE:
return (sw_name, room, u'Chambre resident desactive', VLAN_NOK) return (
sw_name,
room,
u'Utilisateur banni ou desactive -> Web redirect',
None,
False
)
elif not (user.is_connected() or user.is_whitelisted()):
return (
sw_name,
room,
u'Utilisateur non cotisant',
getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
)
# else: user OK, on passe à la verif MAC # else: user OK, on passe à la verif MAC
# Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd # Si on fait de l'auth par mac, on cherche l'interface
# via sa mac dans la bdd
if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT': if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
# Authentification par mac # Authentification par mac
interface = (Interface.objects interface = (Interface.objects
@ -421,88 +485,67 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
.first()) .first())
if not interface: if not interface:
room = port.room room = port.room
# On essaye de register la mac, si l'autocapture a été activée # On essaye de register la mac, si l'autocapture a été activée,
# Sinon on rejette sur vlan_nok # on rejette pour faire une redirection web si possible.
if not nas_type.autocapture_mac: if nas_type.autocapture_mac:
return (sw_name, "", u'Machine inconnue', VLAN_NOK) return (
# On ne peut autocapturer que si on connait la chambre et donc l'user correspondant sw_name,
elif not room: room,
return (sw_name, u'Machine Inconnue -> Web redirect',
"Inconnue", None,
u'Chambre et machine inconnues', False
VLAN_NOK)
else:
# Si la chambre est vide (local club, prises en libre services)
# Impossible d'autocapturer
if not room_user:
room_user = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room)
) )
if not room_user: # Sinon on bascule sur la politique définie dans les options
return (sw_name, # radius.
room,
u'Machine et propriétaire de la chambre '
'inconnus',
VLAN_NOK)
# Si il y a plus d'un user dans la chambre, impossible de savoir à qui
# Ajouter la machine
elif room_user.count() > 1:
return (sw_name,
room,
u'Machine inconnue, il y a au moins 2 users '
'dans la chambre/local -> ajout de mac '
'automatique impossible',
VLAN_NOK)
# Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture
elif not room_user.first().has_access():
return (sw_name,
room,
u'Machine inconnue et adhérent non cotisant',
VLAN_NOK)
# Sinon on capture et on laisse passer sur le bon vlan
else: else:
interface, reason = (room_user return (
.first() sw_name,
.autoregister_machine( "",
mac_address, u'Machine inconnue',
nas_type getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None),
)) RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT
if interface: )
## Si on choisi de placer les machines sur le vlan correspondant à leur type :
if RADIUS_POLICY == 'MACHINE': # L'interface a été trouvée, on vérifie qu'elle est active,
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id # sinon on reject
return (sw_name,
room,
u'Access Ok, Capture de la mac: ' + extra_log,
DECISION_VLAN)
else:
return (sw_name,
room,
u'Erreur dans le register mac %s' % (
reason + str(mac_address)
),
VLAN_NOK)
# L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
# Si elle n'a pas d'ipv4, on lui en met une # Si elle n'a pas d'ipv4, on lui en met une
# Enfin on laisse passer sur le vlan pertinent # Enfin on laisse passer sur le vlan pertinent
else: else:
room = port.room room = port.room
if interface.machine.user.is_ban():
return (
sw_name,
room,
u'Adherent banni',
getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None),
RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT
)
if not interface.is_active: if not interface.is_active:
return (sw_name, return (
sw_name,
room, room,
u'Machine non active / adherent non cotisant', u'Machine non active / adherent non cotisant',
VLAN_NOK) getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None),
## Si on choisi de placer les machines sur le vlan correspondant à leur type : RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT
if RADIUS_POLICY == 'MACHINE': )
# Si on choisi de placer les machines sur le vlan
# correspondant à leur type :
if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE':
DECISION_VLAN = interface.type.ip_type.vlan.vlan_id DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
if not interface.ipv4: if not interface.ipv4:
interface.assign_ipv4() interface.assign_ipv4()
return (sw_name, return (
sw_name,
room, room,
u"Ok, Reassignation de l'ipv4" + extra_log, u"Ok, Reassignation de l'ipv4" + extra_log,
DECISION_VLAN) DECISION_VLAN,
True
)
else: else:
return (sw_name, return (
sw_name,
room, room,
u'Machine OK' + extra_log, u'Machine OK' + extra_log,
DECISION_VLAN) DECISION_VLAN,
True
)

View file

@ -316,6 +316,25 @@ update_django() {
echo "Collecting web frontend statics ..." echo "Collecting web frontend statics ..."
python3 manage.py collectstatic --noinput python3 manage.py collectstatic --noinput
echo "Collecting web frontend statics: Done" echo "Collecting web frontend statics: Done"
echo "Generating locales ..."
python3 manage.py compilemessages
echo "Generating locales: Done"
}
copy_templates_files() {
### Usage: copy_templates_files
#
# This will copy LaTeX templates in the media root.
echo "Copying LaTeX templates ..."
mkdir -p media/templates/
cp cotisations/templates/cotisations/factures.tex media/templates/default_invoice.tex
cp cotisations/templates/cotisations/voucher.tex media/templates/default_voucher.tex
chown -R www-data:www-data media/templates/
echo "Copying LaTeX templates: Done"
} }
@ -744,9 +763,10 @@ main_function() {
echo " * {help} ---------- Display this quick usage documentation" echo " * {help} ---------- Display this quick usage documentation"
echo " * {setup} --------- Launch the full interactive guide to setup entirely" echo " * {setup} --------- Launch the full interactive guide to setup entirely"
echo " re2o from scratch" echo " re2o from scratch"
echo " * {update} -------- Collect frontend statics, install the missing APT" echo " * {update} -------- Collect frontend statics, install the missing APT and copy LaTeX templates files"
echo " and pip packages and apply the migrations to the DB" echo " and pip packages and apply the migrations to the DB"
echo " * {update-django} - Apply Django migration and collect frontend statics" echo " * {update-django} - Apply Django migration and collect frontend statics"
echo " * {copy-template-files} - Copy LaTeX templates files to media/templates"
echo " * {update-packages} Install the missing APT and pip packages" echo " * {update-packages} Install the missing APT and pip packages"
echo " * {update-settings} Interactively rewrite the settings file" echo " * {update-settings} Interactively rewrite the settings file"
echo " * {reset-db} ------ Erase the previous local database, setup a new empty" echo " * {reset-db} ------ Erase the previous local database, setup a new empty"
@ -778,9 +798,14 @@ main_function() {
update ) update )
install_requirements install_requirements
copy_templates_files
update_django update_django
;; ;;
copy-templates-files )
copy_templates_files
;;
update-django ) update-django )
update_django update_django
;; ;;

Binary file not shown.

View file

@ -21,7 +21,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2.5\n" "Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-15 20:12+0200\n" "POT-Creation-Date: 2019-01-08 23:16+0100\n"
"PO-Revision-Date: 2018-06-23 16:01+0200\n" "PO-Revision-Date: 2018-06-23 16:01+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n" "Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n" "Language-Team: \n"
@ -57,7 +57,7 @@ msgstr "Commentaire"
#: templates/logs/aff_stats_logs.html:58 templates/logs/aff_summary.html:62 #: templates/logs/aff_stats_logs.html:58 templates/logs/aff_summary.html:62
#: templates/logs/aff_summary.html:85 templates/logs/aff_summary.html:104 #: templates/logs/aff_summary.html:85 templates/logs/aff_summary.html:104
#: templates/logs/aff_summary.html:123 templates/logs/aff_summary.html:142 #: templates/logs/aff_summary.html:128 templates/logs/aff_summary.html:147
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
@ -113,15 +113,19 @@ msgstr "%(username)s a mis à jour"
#: templates/logs/aff_summary.html:113 #: templates/logs/aff_summary.html:113
#, python-format #, python-format
msgid "%(username)s has sold %(number)sx %(name)s to" msgid "%(username)s has sold %(number)sx %(name)s"
msgstr "%(username)s a vendu %(number)sx %(name)s à" msgstr "%(username)s a vendu %(number)sx %(name)s"
#: templates/logs/aff_summary.html:116 #: templates/logs/aff_summary.html:116
msgid " to"
msgstr " à"
#: templates/logs/aff_summary.html:119
#, python-format #, python-format
msgid "+%(duration)s months" msgid "+%(duration)s months"
msgstr "+%(duration)s mois" msgstr "+%(duration)s mois"
#: templates/logs/aff_summary.html:132 #: templates/logs/aff_summary.html:137
#, python-format #, python-format
msgid "%(username)s has edited an interface of" msgid "%(username)s has edited an interface of"
msgstr "%(username)s a modifié une interface de" msgstr "%(username)s a modifié une interface de"
@ -149,7 +153,7 @@ msgstr "Confirmer"
msgid "Statistics" msgid "Statistics"
msgstr "Statistiques" msgstr "Statistiques"
#: templates/logs/index.html:32 templates/logs/stats_logs.html:32 views.py:403 #: templates/logs/index.html:32 templates/logs/stats_logs.html:32 views.py:414
msgid "Actions performed" msgid "Actions performed"
msgstr "Actions effectuées" msgstr "Actions effectuées"
@ -173,7 +177,7 @@ msgstr "Base de données"
msgid "Wiring actions" msgid "Wiring actions"
msgstr "Actions de câblage" msgstr "Actions de câblage"
#: templates/logs/sidebar.html:53 views.py:325 #: templates/logs/sidebar.html:53 views.py:336
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
@ -189,150 +193,154 @@ msgstr "Statistiques sur la base de données"
msgid "Statistics about users" msgid "Statistics about users"
msgstr "Statistiques sur les utilisateurs" msgstr "Statistiques sur les utilisateurs"
#: views.py:191 #: views.py:194
msgid "Nonexistent revision." msgid "Nonexistent revision."
msgstr "Révision inexistante." msgstr "Révision inexistante."
#: views.py:194 #: views.py:197
msgid "The action was deleted." msgid "The action was deleted."
msgstr "L'action a été supprimée." msgstr "L'action a été supprimée."
#: views.py:227 #: views.py:230
msgid "Category" msgid "Category"
msgstr "Catégorie" msgstr "Catégorie"
#: views.py:228 #: views.py:231
msgid "Number of users (members and clubs)" msgid "Number of users (members and clubs)"
msgstr "Nombre d'utilisateurs (adhérents et clubs)" msgstr "Nombre d'utilisateurs (adhérents et clubs)"
#: views.py:229 #: views.py:232
msgid "Number of members" msgid "Number of members"
msgstr "Nombre d'adhérents" msgstr "Nombre d'adhérents"
#: views.py:230 #: views.py:233
msgid "Number of clubs" msgid "Number of clubs"
msgstr "Nombre de clubs" msgstr "Nombre de clubs"
#: views.py:234 #: views.py:237
msgid "Activated users" msgid "Activated users"
msgstr "Utilisateurs activés" msgstr "Utilisateurs activés"
#: views.py:242 #: views.py:245
msgid "Disabled users" msgid "Disabled users"
msgstr "Utilisateurs désactivés" msgstr "Utilisateurs désactivés"
#: views.py:250 #: views.py:253
msgid "Archived users" msgid "Archived users"
msgstr "Utilisateurs archivés" msgstr "Utilisateurs archivés"
#: views.py:258 #: views.py:261
msgid "Not yet active users"
msgstr "Utilisateurs pas encore actifs"
#: views.py:269
msgid "Contributing members" msgid "Contributing members"
msgstr "Adhérents cotisants" msgstr "Adhérents cotisants"
#: views.py:264 #: views.py:275
msgid "Users benefiting from a connection" msgid "Users benefiting from a connection"
msgstr "Utilisateurs bénéficiant d'une connexion" msgstr "Utilisateurs bénéficiant d'une connexion"
#: views.py:270 #: views.py:281
msgid "Banned users" msgid "Banned users"
msgstr "Utilisateurs bannis" msgstr "Utilisateurs bannis"
#: views.py:276 #: views.py:287
msgid "Users benefiting from a free connection" msgid "Users benefiting from a free connection"
msgstr "Utilisateurs bénéficiant d'une connexion gratuite" msgstr "Utilisateurs bénéficiant d'une connexion gratuite"
#: views.py:282 #: views.py:293
msgid "Active interfaces (with access to the network)" msgid "Active interfaces (with access to the network)"
msgstr "Interfaces actives (ayant accès au réseau)" msgstr "Interfaces actives (ayant accès au réseau)"
#: views.py:292 #: views.py:303
msgid "Active interfaces assigned IPv4" msgid "Active interfaces assigned IPv4"
msgstr "Interfaces actives assignées IPv4" msgstr "Interfaces actives assignées IPv4"
#: views.py:305 #: views.py:316
msgid "IP range" msgid "IP range"
msgstr "Plage d'IP" msgstr "Plage d'IP"
#: views.py:306 #: views.py:317
msgid "VLAN" msgid "VLAN"
msgstr "VLAN" msgstr "VLAN"
#: views.py:307 #: views.py:318
msgid "Total number of IP addresses" msgid "Total number of IP addresses"
msgstr "Nombre total d'adresses IP" msgstr "Nombre total d'adresses IP"
#: views.py:308 #: views.py:319
msgid "Number of assigned IP addresses" msgid "Number of assigned IP addresses"
msgstr "Nombre d'adresses IP non assignées" msgstr "Nombre d'adresses IP non assignées"
#: views.py:309 #: views.py:320
msgid "Number of IP address assigned to an activated machine" msgid "Number of IP address assigned to an activated machine"
msgstr "Nombre d'adresses IP assignées à une machine activée" msgstr "Nombre d'adresses IP assignées à une machine activée"
#: views.py:310 #: views.py:321
msgid "Number of nonassigned IP addresses" msgid "Number of nonassigned IP addresses"
msgstr "Nombre d'adresses IP non assignées" msgstr "Nombre d'adresses IP non assignées"
#: views.py:337 #: views.py:348
msgid "Subscriptions" msgid "Subscriptions"
msgstr "Cotisations" msgstr "Cotisations"
#: views.py:359 views.py:420 #: views.py:370 views.py:431
msgid "Machines" msgid "Machines"
msgstr "Machines" msgstr "Machines"
#: views.py:386 #: views.py:397
msgid "Topology" msgid "Topology"
msgstr "Topologie" msgstr "Topologie"
#: views.py:405 #: views.py:416
msgid "Number of actions" msgid "Number of actions"
msgstr "Nombre d'actions" msgstr "Nombre d'actions"
#: views.py:419 views.py:437 views.py:442 views.py:447 views.py:462 #: views.py:430 views.py:448 views.py:453 views.py:458 views.py:473
msgid "User" msgid "User"
msgstr "Utilisateur" msgstr "Utilisateur"
#: views.py:423 #: views.py:434
msgid "Invoice" msgid "Invoice"
msgstr "Facture" msgstr "Facture"
#: views.py:426 #: views.py:437
msgid "Ban" msgid "Ban"
msgstr "Bannissement" msgstr "Bannissement"
#: views.py:429 #: views.py:440
msgid "Whitelist" msgid "Whitelist"
msgstr "Accès gracieux" msgstr "Accès gracieux"
#: views.py:432 #: views.py:443
msgid "Rights" msgid "Rights"
msgstr "Droits" msgstr "Droits"
#: views.py:436 #: views.py:447
msgid "School" msgid "School"
msgstr "Établissement" msgstr "Établissement"
#: views.py:441 #: views.py:452
msgid "Payment method" msgid "Payment method"
msgstr "Moyen de paiement" msgstr "Moyen de paiement"
#: views.py:446 #: views.py:457
msgid "Bank" msgid "Bank"
msgstr "Banque" msgstr "Banque"
#: views.py:463 #: views.py:474
msgid "Action" msgid "Action"
msgstr "Action" msgstr "Action"
#: views.py:494 #: views.py:505
msgid "No model found." msgid "No model found."
msgstr "Aucun modèle trouvé." msgstr "Aucun modèle trouvé."
#: views.py:500 #: views.py:511
msgid "Nonexistent entry." msgid "Nonexistent entry."
msgstr "Entrée inexistante." msgstr "Entrée inexistante."
#: views.py:507 #: views.py:518
msgid "You don't have the right to access this menu." msgid "You don't have the right to access this menu."
msgstr "Vous n'avez pas le droit d'accéder à ce menu." msgstr "Vous n'avez pas le droit d'accéder à ce menu."

View file

@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% if revisions_list.paginator %} {% if revisions_list.paginator %}
{% include "pagination.html" with list=revisions_list %} {% include 'pagination.html' with list=revisions_list %}
{% endif %} {% endif %}
{% load logs_extra %} {% load logs_extra %}
@ -36,9 +36,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% trans "Edited object" %}</th> <th>{% trans "Edited object" %}</th>
<th>{% trans "Object type" %}</th> <th>{% trans "Object type" %}</th>
{% trans "Edited by" as tr_edited_by %} {% trans "Edited by" as tr_edited_by %}
<th>{% include "buttons/sort.html" with prefix='logs' col='author' text=tr_edited_by %}</th> <th>{% include 'buttons/sort.html' with prefix='logs' col='author' text=tr_edited_by %}</th>
{% trans "Date of editing" as tr_date_of_editing %} {% trans "Date of editing" as tr_date_of_editing %}
<th>{% include "buttons/sort.html" with prefix='logs' col='date' text=tr_date_of_editing %}</th> <th>{% include 'buttons/sort.html' with prefix='logs' col='date' text=tr_date_of_editing %}</th>
<th>{% trans "Comment" %}</th> <th>{% trans "Comment" %}</th>
<th></th> <th></th>
</tr> </tr>
@ -65,6 +65,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</table> </table>
{% if revisions_list.paginator %} {% if revisions_list.paginator %}
{% include "pagination.html" with list=revisions_list %} {% include 'pagination.html' with list=revisions_list %}
{% endif %} {% endif %}

View file

@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% if versions_list.paginator %} {% if versions_list.paginator %}
{% include "pagination.html" with list=versions_list %} {% include 'pagination.html' with list=versions_list %}
{% endif %} {% endif %}
{% load logs_extra %} {% load logs_extra %}
@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<thead> <thead>
<tr> <tr>
{% trans "Date" as tr_date %} {% trans "Date" as tr_date %}
<th>{% include "buttons/sort.html" with prefix='sum' col='date' text=tr_date %}</th> <th>{% include 'buttons/sort.html' with prefix='sum' col='date' text=tr_date %}</th>
<th>{% trans "Editing" %}</th> <th>{% trans "Editing" %}</th>
<th></th> <th></th>
</tr> </tr>
@ -154,6 +154,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</table> </table>
{% if versions_list.paginator %} {% if versions_list.paginator %}
{% include "pagination.html" with list=versions_list %} {% include 'pagination.html' with list=versions_list %}
{% endif %} {% endif %}

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %} {% extends 'logs/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

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %} {% extends 'logs/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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h2>{% trans "Actions performed" %}</h2> <h2>{% trans "Actions performed" %}</h2>
{% include "logs/aff_summary.html" with versions_list=versions_list %} {% include 'logs/aff_summary.html' with versions_list=versions_list %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends 'base.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
@ -28,27 +28,27 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block sidebar %} {% block sidebar %}
{% can_view_app logs %} {% can_view_app logs %}
<a class="list-group-item list-group-item-info" href="{% url "logs:index" %}"> <a class="list-group-item list-group-item-info" href="{% url 'logs:index' %}">
<i class="fa fa-clipboard-list"></i> <i class="fa fa-clipboard"></i>
{% trans "Summary" %} {% trans "Summary" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-logs" %}"> <a class="list-group-item list-group-item-info" href="{% url 'logs:stats-logs' %}">
<i class="fa fa-calendar-alt"></i> <i class="fa fa-calendar"></i>
{% trans "Events" %} {% trans "Events" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-general" %}"> <a class="list-group-item list-group-item-info" href="{% url 'logs:stats-general' %}">
<i class="fa fa-chart-area"></i> <i class="fa fa-area-chart"></i>
{% trans "General" %} {% trans "General" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-models" %}"> <a class="list-group-item list-group-item-info" href="{% url 'logs:stats-models' %}">
<i class="fa fa-database"></i> <i class="fa fa-database"></i>
{% trans "Database" %} {% trans "Database" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-actions" %}"> <a class="list-group-item list-group-item-info" href="{% url 'logs:stats-actions' %}">
<i class="fa fa-plug"></i> <i class="fa fa-plug"></i>
{% trans "Wiring actions" %} {% trans "Wiring actions" %}
</a> </a>
<a class="list-group-item list-group-item-info" href="{% url "logs:stats-users" %}"> <a class="list-group-item list-group-item-info" href="{% url 'logs:stats-users' %}">
<i class="fa fa-users"></i> <i class="fa fa-users"></i>
{% trans "Users" %} {% trans "Users" %}
</a> </a>

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %} {% extends 'logs/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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h2>{% trans "General statistics" %}</h2> <h2>{% trans "General statistics" %}</h2>
{% include "logs/aff_stats_general.html" with stats_list=stats_list %} {% include 'logs/aff_stats_general.html' with stats_list=stats_list %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %} {% extends 'logs/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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h2>{% trans "Actions performed" %}</h2> <h2>{% trans "Actions performed" %}</h2>
{% include "logs/aff_stats_logs.html" with revisions_list=revisions_list %} {% include 'logs/aff_stats_logs.html' with revisions_list=revisions_list %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %} {% extends 'logs/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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h2>{% trans "Database statistics" %}</h2> <h2>{% trans "Database statistics" %}</h2>
{% include "logs/aff_stats_models.html" with stats_list=stats_list %} {% include 'logs/aff_stats_models.html' with stats_list=stats_list %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %} {% extends 'logs/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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h2>{% trans "Statistics about users" %}</h2> <h2>{% trans "Statistics about users" %}</h2>
{% include "logs/aff_stats_users.html" with stats_list=stats_list %} {% include 'logs/aff_stats_users.html' with stats_list=stats_list %}
<br /> <br />
<br /> <br />
<br /> <br />

View file

@ -102,15 +102,18 @@ from re2o.utils import (
all_baned, all_baned,
all_has_access, all_has_access,
all_adherent, all_adherent,
all_active_assigned_interfaces_count,
all_active_interfaces_count,
)
from re2o.base import (
re2o_paginator, re2o_paginator,
SortTable
) )
from re2o.acl import ( from re2o.acl import (
can_view_all, can_view_all,
can_view_app, can_view_app,
can_edit_history, can_edit_history,
) )
from re2o.utils import all_active_assigned_interfaces_count
from re2o.utils import all_active_interfaces_count, SortTable
@login_required @login_required
@ -254,6 +257,14 @@ def stats_general(request):
.count()), .count()),
Club.objects.filter(state=Club.STATE_ARCHIVE).count() Club.objects.filter(state=Club.STATE_ARCHIVE).count()
], ],
'not_active_users': [
_("Not yet active users"),
User.objects.filter(state=User.STATE_NOT_YET_ACTIVE).count(),
(Adherent.objects
.filter(state=Adherent.STATE_NOT_YET_ACTIVE)
.count()),
Club.objects.filter(state=Club.STATE_NOT_YET_ACTIVE).count()
],
'adherent_users': [ 'adherent_users': [
_("Contributing members"), _("Contributing members"),
_all_adherent.count(), _all_adherent.count(),

View file

@ -41,4 +41,3 @@ def can_view(user):
can = user.has_module_perms('machines') can = user.has_module_perms('machines')
return can, None if can else _("You don't have the right to view this" return can, None if can else _("You don't have the right to view this"
" application.") " application.")

View file

@ -29,7 +29,6 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import IpType, Machine, MachineType, Domain, IpList, Interface
from .models import ( from .models import (
Extension, Extension,
SOA, SOA,
@ -47,6 +46,7 @@ from .models import (
Ipv6List, Ipv6List,
OuverturePortList, OuverturePortList,
) )
from .models import IpType, Machine, MachineType, Domain, IpList, Interface
class MachineAdmin(VersionAdmin): class MachineAdmin(VersionAdmin):
@ -98,6 +98,7 @@ class TxtAdmin(VersionAdmin):
""" Admin view of a TXT object """ """ Admin view of a TXT object """
pass pass
class DNameAdmin(VersionAdmin): class DNameAdmin(VersionAdmin):
""" Admin view of a DName object """ """ Admin view of a DName object """
pass pass
@ -147,12 +148,12 @@ class ServiceAdmin(VersionAdmin):
""" Admin view of a ServiceAdmin object """ """ Admin view of a ServiceAdmin object """
list_display = ('service_type', 'min_time_regen', 'regular_time_regen') list_display = ('service_type', 'min_time_regen', 'regular_time_regen')
class RoleAdmin(VersionAdmin): class RoleAdmin(VersionAdmin):
""" Admin view of a RoleAdmin object """ """ Admin view of a RoleAdmin object """
pass pass
admin.site.register(Machine, MachineAdmin) admin.site.register(Machine, MachineAdmin)
admin.site.register(MachineType, MachineTypeAdmin) admin.site.register(MachineType, MachineTypeAdmin)
admin.site.register(IpType, IpTypeAdmin) admin.site.register(IpType, IpTypeAdmin)

View file

@ -35,13 +35,12 @@ Formulaires d'ajout, edition et suppressions de :
from __future__ import unicode_literals from __future__ import unicode_literals
from django.forms import ModelForm, Form
from django import forms from django import forms
from django.forms import ModelForm, Form
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
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 ( from .models import (
Domain, Domain,
Machine, Machine,
@ -68,6 +67,7 @@ from .models import (
class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"""Formulaire d'édition d'une machine""" """Formulaire d'édition d'une machine"""
class Meta: class Meta:
model = Machine model = Machine
fields = '__all__' fields = '__all__'
@ -80,12 +80,14 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class NewMachineForm(EditMachineForm): class NewMachineForm(EditMachineForm):
"""Creation d'une machine, ne renseigne que le nom""" """Creation d'une machine, ne renseigne que le nom"""
class Meta(EditMachineForm.Meta): class Meta(EditMachineForm.Meta):
fields = ['name'] fields = ['name']
class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"""Edition d'une interface. Edition complète""" """Edition d'une interface. Edition complète"""
class Meta: class Meta:
model = Interface model = Interface
fields = ['machine', 'type', 'ipv4', 'mac_address', 'details'] fields = ['machine', 'type', 'ipv4', 'mac_address', 'details']
@ -128,12 +130,14 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class AddInterfaceForm(EditInterfaceForm): class AddInterfaceForm(EditInterfaceForm):
"""Ajout d'une interface à une machine. En fonction des droits, """Ajout d'une interface à une machine. En fonction des droits,
affiche ou non l'ensemble des ip disponibles""" affiche ou non l'ensemble des ip disponibles"""
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['type', 'ipv4', 'mac_address', 'details'] fields = ['type', 'ipv4', 'mac_address', 'details']
class AliasForm(FormRevMixin, ModelForm): class AliasForm(FormRevMixin, ModelForm):
"""Ajout d'un alias (et edition), CNAME, contenant nom et extension""" """Ajout d'un alias (et edition), CNAME, contenant nom et extension"""
class Meta: class Meta:
model = Domain model = Domain
fields = ['name', 'extension'] fields = ['name', 'extension']
@ -151,6 +155,7 @@ class AliasForm(FormRevMixin, ModelForm):
class DomainForm(FormRevMixin, ModelForm): class DomainForm(FormRevMixin, ModelForm):
"""Ajout et edition d'un enregistrement de nom, relié à interface""" """Ajout et edition d'un enregistrement de nom, relié à interface"""
class Meta: class Meta:
model = Domain model = Domain
fields = ['name'] fields = ['name']
@ -183,6 +188,7 @@ class DelAliasForm(FormRevMixin, Form):
class MachineTypeForm(FormRevMixin, ModelForm): class MachineTypeForm(FormRevMixin, ModelForm):
"""Ajout et edition d'un machinetype, relié à un iptype""" """Ajout et edition d'un machinetype, relié à un iptype"""
class Meta: class Meta:
model = MachineType model = MachineType
fields = ['type', 'ip_type'] fields = ['type', 'ip_type']
@ -214,6 +220,7 @@ class DelMachineTypeForm(FormRevMixin, Form):
class IpTypeForm(FormRevMixin, ModelForm): class IpTypeForm(FormRevMixin, ModelForm):
"""Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de """Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de
stop après creation""" stop après creation"""
class Meta: class Meta:
model = IpType model = IpType
fields = '__all__' fields = '__all__'
@ -227,6 +234,7 @@ class IpTypeForm(FormRevMixin, ModelForm):
class EditIpTypeForm(IpTypeForm): class EditIpTypeForm(IpTypeForm):
"""Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait """Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait
synchroniser les objets iplist""" synchroniser les objets iplist"""
class Meta(IpTypeForm.Meta): class Meta(IpTypeForm.Meta):
fields = ['extension', 'type', 'need_infra', 'domaine_ip_network', 'domaine_ip_netmask', fields = ['extension', 'type', 'need_infra', 'domaine_ip_network', 'domaine_ip_netmask',
'prefix_v6', 'prefix_v6_length', 'prefix_v6', 'prefix_v6_length',
@ -253,6 +261,7 @@ class DelIpTypeForm(FormRevMixin, Form):
class ExtensionForm(FormRevMixin, ModelForm): class ExtensionForm(FormRevMixin, ModelForm):
"""Formulaire d'ajout et edition d'une extension""" """Formulaire d'ajout et edition d'une extension"""
class Meta: class Meta:
model = Extension model = Extension
fields = '__all__' fields = '__all__'
@ -264,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm):
self.fields['origin'].label = _("A record origin") self.fields['origin'].label = _("A record origin")
self.fields['origin_v6'].label = _("AAAA record origin") self.fields['origin_v6'].label = _("AAAA record origin")
self.fields['soa'].label = _("SOA record to use") self.fields['soa'].label = _("SOA record to use")
self.fields['dnssec'].label = _("Sign with DNSSEC")
class DelExtensionForm(FormRevMixin, Form): class DelExtensionForm(FormRevMixin, Form):
@ -285,6 +295,7 @@ class DelExtensionForm(FormRevMixin, Form):
class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"""Gestion des ipv6 d'une machine""" """Gestion des ipv6 d'une machine"""
class Meta: class Meta:
model = Ipv6List model = Ipv6List
fields = ['ipv6', 'slaac_ip'] fields = ['ipv6', 'slaac_ip']
@ -296,6 +307,7 @@ class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class SOAForm(FormRevMixin, ModelForm): class SOAForm(FormRevMixin, ModelForm):
"""Ajout et edition d'un SOA""" """Ajout et edition d'un SOA"""
class Meta: class Meta:
model = SOA model = SOA
fields = '__all__' fields = '__all__'
@ -324,6 +336,7 @@ class DelSOAForm(FormRevMixin, Form):
class MxForm(FormRevMixin, ModelForm): class MxForm(FormRevMixin, ModelForm):
"""Ajout et edition d'un MX""" """Ajout et edition d'un MX"""
class Meta: class Meta:
model = Mx model = Mx
fields = ['zone', 'priority', 'name'] fields = ['zone', 'priority', 'name']
@ -357,6 +370,7 @@ class NsForm(FormRevMixin, ModelForm):
"""Ajout d'un NS pour une zone """Ajout d'un NS pour une zone
On exclue les CNAME dans les objets domain (interdit par la rfc) On exclue les CNAME dans les objets domain (interdit par la rfc)
donc on prend uniquemet """ donc on prend uniquemet """
class Meta: class Meta:
model = Ns model = Ns
fields = ['zone', 'ns'] fields = ['zone', 'ns']
@ -388,6 +402,7 @@ class DelNsForm(FormRevMixin, Form):
class TxtForm(FormRevMixin, ModelForm): class TxtForm(FormRevMixin, ModelForm):
"""Ajout d'un txt pour une zone""" """Ajout d'un txt pour une zone"""
class Meta: class Meta:
model = Txt model = Txt
fields = '__all__' fields = '__all__'
@ -416,6 +431,7 @@ class DelTxtForm(FormRevMixin, Form):
class DNameForm(FormRevMixin, ModelForm): class DNameForm(FormRevMixin, ModelForm):
"""Add a DNAME entry for a zone""" """Add a DNAME entry for a zone"""
class Meta: class Meta:
model = DName model = DName
fields = '__all__' fields = '__all__'
@ -444,6 +460,7 @@ class DelDNameForm(FormRevMixin, Form):
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:
model = Srv model = Srv
fields = '__all__' fields = '__all__'
@ -473,6 +490,7 @@ class DelSrvForm(FormRevMixin, Form):
class NasForm(FormRevMixin, ModelForm): class NasForm(FormRevMixin, ModelForm):
"""Ajout d'un type de nas (machine d'authentification, """Ajout d'un type de nas (machine d'authentification,
swicths, bornes...)""" swicths, bornes...)"""
class Meta: class Meta:
model = Nas model = Nas
fields = '__all__' fields = '__all__'
@ -501,6 +519,7 @@ class DelNasForm(FormRevMixin, Form):
class RoleForm(FormRevMixin, ModelForm): class RoleForm(FormRevMixin, ModelForm):
"""Add and edit role.""" """Add and edit role."""
class Meta: class Meta:
model = Role model = Role
fields = '__all__' fields = '__all__'
@ -533,6 +552,7 @@ class DelRoleForm(FormRevMixin, Form):
class ServiceForm(FormRevMixin, ModelForm): class ServiceForm(FormRevMixin, ModelForm):
"""Ajout et edition d'une classe de service : dns, dhcp, etc""" """Ajout et edition d'une classe de service : dns, dhcp, etc"""
class Meta: class Meta:
model = Service model = Service
fields = '__all__' fields = '__all__'
@ -574,15 +594,27 @@ class DelServiceForm(FormRevMixin, Form):
class VlanForm(FormRevMixin, ModelForm): class VlanForm(FormRevMixin, ModelForm):
"""Ajout d'un vlan : id, nom""" """Ajout d'un vlan : id, nom"""
class Meta: class Meta:
model = Vlan model = Vlan
fields = '__all__' fields = ['vlan_id', 'name', 'comment']
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(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs)
class EditOptionVlanForm(FormRevMixin, ModelForm):
"""Ajout d'un vlan : id, nom"""
class Meta:
model = Vlan
fields = ['dhcp_snooping', 'dhcpv6_snooping', 'arp_protect', 'igmp', 'mld']
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EditOptionVlanForm, self).__init__(*args, prefix=prefix, **kwargs)
class DelVlanForm(FormRevMixin, Form): class DelVlanForm(FormRevMixin, Form):
"""Suppression d'un ou plusieurs vlans""" """Suppression d'un ou plusieurs vlans"""
vlan = forms.ModelMultipleChoiceField( vlan = forms.ModelMultipleChoiceField(
@ -603,6 +635,7 @@ class DelVlanForm(FormRevMixin, Form):
class EditOuverturePortConfigForm(FormRevMixin, ModelForm): class EditOuverturePortConfigForm(FormRevMixin, ModelForm):
"""Edition de la liste des profils d'ouverture de ports """Edition de la liste des profils d'ouverture de ports
pour l'interface""" pour l'interface"""
class Meta: class Meta:
model = Interface model = Interface
fields = ['port_lists'] fields = ['port_lists']
@ -619,6 +652,7 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm):
class EditOuverturePortListForm(FormRevMixin, ModelForm): class EditOuverturePortListForm(FormRevMixin, ModelForm):
"""Edition de la liste des ports et profils d'ouverture """Edition de la liste des ports et profils d'ouverture
des ports""" des ports"""
class Meta: class Meta:
model = OuverturePortList model = OuverturePortList
fields = '__all__' fields = '__all__'
@ -634,6 +668,7 @@ class EditOuverturePortListForm(FormRevMixin, ModelForm):
class SshFpForm(FormRevMixin, ModelForm): class SshFpForm(FormRevMixin, ModelForm):
"""Edits a SSHFP record.""" """Edits a SSHFP record."""
class Meta: class Meta:
model = SshFp model = SshFp
exclude = ('machine',) exclude = ('machine',)
@ -645,4 +680,3 @@ class SshFpForm(FormRevMixin, ModelForm):
prefix=prefix, prefix=prefix,
**kwargs **kwargs
) )

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-09-19 20:25
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0094_auto_20180815_1918'),
]
operations = [
migrations.AddField(
model_name='vlan',
name='arp_protect',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='vlan',
name='dhcp_snooping',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='vlan',
name='dhcpv6_snooping',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='vlan',
name='igmp',
field=models.BooleanField(default=False, help_text='Gestion multicast v4'),
),
migrations.AddField(
model_name='vlan',
name='mld',
field=models.BooleanField(default=False, help_text='Gestion multicast v6'),
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-10-13 12:17
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('machines', '0095_auto_20180919_2225'),
]
operations = [
migrations.AlterField(
model_name='machine',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-12-24 14:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0096_auto_20181013_1417'),
]
operations = [
migrations.AddField(
model_name='extension',
name='dnssec',
field=models.BooleanField(default=False, help_text='Should the zone be signed with DNSSEC'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0097_extension_dnssec'),
]
operations = [
migrations.AlterField(
model_name='role',
name='specific_role',
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:45
from __future__ import unicode_literals
from django.db import migrations, models
def migrate(apps, schema_editor):
Role = apps.get_model('machines', 'Role')
for role in Role.objects.filter(specific_role='dns-recursif-server'):
role.specific_role = 'dns-recursive-server'
role.save()
class Migration(migrations.Migration):
dependencies = [
('machines', '0098_auto_20190102_1745'),
]
operations = [
migrations.RunPython(migrate),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-02 23:53
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0099_role_recursive_dns'),
]
operations = [
migrations.AlterField(
model_name='role',
name='specific_role',
field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursive-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True),
),
]

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-08 22:23
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0100_auto_20190102_1753'),
]
operations = [
migrations.AlterModelOptions(
name='ouvertureport',
options={'verbose_name': 'ports opening', 'verbose_name_plural': 'ports openings'},
),
migrations.AlterField(
model_name='nas',
name='port_access_mode',
field=models.CharField(choices=[('802.1X', '802.1X'), ('Mac-address', 'MAC-address')], default='802.1X', max_length=32),
),
migrations.AlterField(
model_name='vlan',
name='igmp',
field=models.BooleanField(default=False, help_text='v4 multicast management'),
),
migrations.AlterField(
model_name='vlan',
name='mld',
field=models.BooleanField(default=False, help_text='v6 multicast management'),
),
]

View file

@ -27,37 +27,35 @@ The models definitions for the Machines app
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import timedelta import base64
import hashlib
import re import re
from datetime import timedelta
from ipaddress import IPv6Address from ipaddress import IPv6Address
from itertools import chain from itertools import chain
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
import hashlib
import base64
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.forms import ValidationError from django.forms import ValidationError
from django.utils.functional import cached_property
from django.utils import timezone from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from macaddress.fields import MACAddressField, default_dialect
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
from macaddress.fields import MACAddressField import preferences.models
import users.models
from re2o.field_permissions import FieldPermissionModelMixin from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin from re2o.mixins import AclMixin, RevMixin
import users.models
import preferences.models
class Machine(RevMixin, FieldPermissionModelMixin, models.Model): class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
""" Class définissant une machine, object parent user, objets fils """ Class définissant une machine, object parent user, objets fils
interfaces""" interfaces"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT) user = models.ForeignKey('users.User', on_delete=models.CASCADE)
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
help_text=_("Optional"), help_text=_("Optional"),
@ -199,7 +197,17 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
def short_name(self): def short_name(self):
"""Par defaut, renvoie le nom de la première interface """Par defaut, renvoie le nom de la première interface
de cette machine""" de cette machine"""
return str(self.interface_set.first().domain.name) interfaces_set = self.interface_set.first()
if interfaces_set:
return str(interfaces_set.domain.name)
else:
return _("No name")
@cached_property
def complete_name(self):
"""Par defaut, renvoie le nom de la première interface
de cette machine"""
return str(self.interface_set.first())
@cached_property @cached_property
def all_short_names(self): def all_short_names(self):
@ -209,6 +217,11 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
interface_parent__machine=self interface_parent__machine=self
).values_list('name', flat=True).distinct() ).values_list('name', flat=True).distinct()
@cached_property
def get_name(self):
"""Return a name : user provided name or first interface name"""
return self.name or self.short_name
@cached_property @cached_property
def all_complete_names(self): def all_complete_names(self):
"""Renvoie tous les tls complets de la machine""" """Renvoie tous les tls complets de la machine"""
@ -327,7 +340,7 @@ class IpType(RevMixin, AclMixin, models.Model):
("use_all_iptype", _("Can use all IP types")), ("use_all_iptype", _("Can use all IP types")),
) )
verbose_name = _("IP type") verbose_name = _("IP type")
verbose_name_plural = ("IP types") verbose_name_plural = _("IP types")
@cached_property @cached_property
def ip_range(self): def ip_range(self):
@ -515,6 +528,18 @@ class Vlan(RevMixin, AclMixin, models.Model):
vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)]) vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
comment = models.CharField(max_length=256, blank=True) comment = models.CharField(max_length=256, blank=True)
#Réglages supplémentaires
arp_protect = models.BooleanField(default=False)
dhcp_snooping = models.BooleanField(default=False)
dhcpv6_snooping = models.BooleanField(default=False)
igmp = models.BooleanField(
default=False,
help_text=_("v4 multicast management")
)
mld = models.BooleanField(
default=False,
help_text=_("v6 multicast management")
)
class Meta: class Meta:
permissions = ( permissions = (
@ -534,7 +559,7 @@ class Nas(RevMixin, AclMixin, models.Model):
default_mode = '802.1X' default_mode = '802.1X'
AUTH = ( AUTH = (
('802.1X', '802.1X'), ('802.1X', '802.1X'),
('Mac-address', 'Mac-address'), ('Mac-address', _("MAC-address")),
) )
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@ -641,7 +666,7 @@ class SOA(RevMixin, AclMixin, models.Model):
utilisée dans les migrations de la BDD. """ utilisée dans les migrations de la BDD. """
return cls.objects.get_or_create( return cls.objects.get_or_create(
name=_("SOA to edit"), name=_("SOA to edit"),
mail="postmaser@example.com" mail="postmaster@example.com"
)[0].pk )[0].pk
@ -671,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model):
'SOA', 'SOA',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
dnssec = models.BooleanField(
default=False,
help_text=_("Should the zone be signed with DNSSEC")
)
class Meta: class Meta:
permissions = ( permissions = (
@ -716,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model):
.filter(cname__interface_parent__in=all_active_assigned_interfaces()) .filter(cname__interface_parent__in=all_active_assigned_interfaces())
.prefetch_related('cname')) .prefetch_related('cname'))
def get_associated_dname_records(self):
return (DName.objects.filter(alias=self))
@staticmethod @staticmethod
def can_use_all(user_request, *_args, **_kwargs): def can_use_all(user_request, *_args, **_kwargs):
"""Superdroit qui permet d'utiliser toutes les extensions sans """Superdroit qui permet d'utiliser toutes les extensions sans
@ -902,7 +934,7 @@ class SshFp(RevMixin, AclMixin, models.Model):
machine = models.ForeignKey('Machine', on_delete=models.CASCADE) machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
pub_key_entry = models.TextField( pub_key_entry = models.TextField(
help_text="SSH public key", help_text=_("SSH public key"),
max_length=2048 max_length=2048
) )
algo = models.CharField( algo = models.CharField(
@ -910,7 +942,7 @@ class SshFp(RevMixin, AclMixin, models.Model):
max_length=32 max_length=32
) )
comment = models.CharField( comment = models.CharField(
help_text="Comment", help_text=_("Comment"),
max_length=255, max_length=255,
null=True, null=True,
blank=True blank=True
@ -955,7 +987,6 @@ class SshFp(RevMixin, AclMixin, models.Model):
return str(self.algo) + ' ' + str(self.comment) return str(self.algo) + ' ' + str(self.comment)
class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
""" Une interface. Objet clef de l'application machine : """ Une interface. Objet clef de l'application machine :
- une address mac unique. Possibilité de la rendre unique avec le - une address mac unique. Possibilité de la rendre unique avec le
@ -1065,7 +1096,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.get_cached_value('ipv6_mode') == 'DHCPV6'): .get_cached_value('ipv6_mode') == 'DHCPV6'):
return self.ipv6list.filter(slaac_ip=False) return self.ipv6list.filter(slaac_ip=False)
else: else:
return None return []
def mac_bare(self): def mac_bare(self):
""" Formatage de la mac type mac_bare""" """ Formatage de la mac type mac_bare"""
@ -1075,28 +1106,10 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
""" Tente un formatage mac_bare, si échoue, lève une erreur de """ Tente un formatage mac_bare, si échoue, lève une erreur de
validation""" validation"""
try: try:
self.mac_address = str(EUI(self.mac_address)) self.mac_address = str(EUI(self.mac_address, dialect=default_dialect()))
except: except:
raise ValidationError(_("The given MAC address is invalid.")) raise ValidationError(_("The given MAC address is invalid."))
def clean(self, *args, **kwargs):
""" Formate l'addresse mac en mac_bare (fonction filter_mac)
et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
# If type was an invalid value, django won't create an attribute type
# but try clean() as we may be able to create it from another value
# so even if the error as yet been detected at this point, django
# continues because the error might not prevent us from creating the
# instance.
# But in our case, it's impossible to create a type value so we raise
# the error.
if not hasattr(self, 'type'):
raise ValidationError(_("The selected IP type is invalid."))
self.filter_macaddress()
self.mac_address = str(EUI(self.mac_address)) or None
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
self.assign_ipv4()
super(Interface, self).clean(*args, **kwargs)
def assign_ipv4(self): def assign_ipv4(self):
""" Assigne une ip à l'interface """ """ Assigne une ip à l'interface """
free_ips = self.type.ip_type.free_ip() free_ips = self.type.ip_type.free_ip()
@ -1116,6 +1129,42 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
self.clean() self.clean()
self.save() self.save()
def has_private_ip(self):
""" True si l'ip associée est privée"""
if self.ipv4:
return IPAddress(str(self.ipv4)).is_private()
else:
return False
def may_have_port_open(self):
""" True si l'interface a une ip et une ip publique.
Permet de ne pas exporter des ouvertures sur des ip privées
(useless)"""
return self.ipv4 and not self.has_private_ip()
def clean(self, *args, **kwargs):
""" Formate l'addresse mac en mac_bare (fonction filter_mac)
et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
# If type was an invalid value, django won't create an attribute type
# but try clean() as we may be able to create it from another value
# so even if the error as yet been detected at this point, django
# continues because the error might not prevent us from creating the
# instance.
# But in our case, it's impossible to create a type value so we raise
# the error.
if not hasattr(self, 'type'):
raise ValidationError(_("The selected IP type is invalid."))
self.filter_macaddress()
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
self.assign_ipv4()
super(Interface, self).clean(*args, **kwargs)
def validate_unique(self, *args, **kwargs):
super(Interface, self).validate_unique(*args, **kwargs)
interfaces_similar = Interface.objects.filter(mac_address=self.mac_address, type__ip_type=self.type.ip_type)
if interfaces_similar and interfaces_similar.first() != self:
raise ValidationError(_("Mac address already registered in this Machine Type/Subnet"))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.filter_macaddress() self.filter_macaddress()
# On verifie la cohérence en forçant l'extension par la méthode # On verifie la cohérence en forçant l'extension par la méthode
@ -1123,6 +1172,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
if self.type.ip_type != self.ipv4.ip_type: if self.type.ip_type != self.ipv4.ip_type:
raise ValidationError(_("The IPv4 address and the machine type" raise ValidationError(_("The IPv4 address and the machine type"
" don't match.")) " don't match."))
self.validate_unique()
super(Interface, self).save(*args, **kwargs) super(Interface, self).save(*args, **kwargs)
@staticmethod @staticmethod
@ -1220,19 +1270,6 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
domain = None domain = None
return str(domain) return str(domain)
def has_private_ip(self):
""" True si l'ip associée est privée"""
if self.ipv4:
return IPAddress(str(self.ipv4)).is_private()
else:
return False
def may_have_port_open(self):
""" True si l'interface a une ip et une ip publique.
Permet de ne pas exporter des ouvertures sur des ip privées
(useless)"""
return self.ipv4 and not self.has_private_ip()
class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
""" A list of IPv6 """ """ A list of IPv6 """
@ -1350,7 +1387,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.filter(interface=self.interface, slaac_ip=True) .filter(interface=self.interface, slaac_ip=True)
.exclude(id=self.id)): .exclude(id=self.id)):
raise ValidationError(_("A SLAAC IP address is already registered.")) raise ValidationError(_("A SLAAC IP address is already registered."))
try:
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
except AttributeError: # Prevents from crashing when there is no defined prefix_v6
prefix_v6 = None
if prefix_v6: if prefix_v6:
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] != if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
IPv6Address(prefix_v6).exploded[:20]): IPv6Address(prefix_v6).exploded[:20]):
@ -1579,7 +1619,7 @@ class Role(RevMixin, AclMixin, models.Model):
ROLE = ( ROLE = (
('dhcp-server', _("DHCP server")), ('dhcp-server', _("DHCP server")),
('switch-conf-server', _("Switches configuration server")), ('switch-conf-server', _("Switches configuration server")),
('dns-recursif-server', _("Recursive DNS server")), ('dns-recursive-server', _("Recursive DNS server")),
('ntp-server', _("NTP server")), ('ntp-server', _("NTP server")),
('radius-server', _("RADIUS server")), ('radius-server', _("RADIUS server")),
('log-server', _("Log server")), ('log-server', _("Log server")),
@ -1608,18 +1648,6 @@ class Role(RevMixin, AclMixin, models.Model):
verbose_name = _("server role") verbose_name = _("server role")
verbose_name_plural = _("server roles") verbose_name_plural = _("server roles")
@classmethod
def get_instance(cls, roleid, *_args, **_kwargs):
"""Get the Role instance with roleid.
Args:
roleid: The id
Returns:
The role.
"""
return cls.objects.get(pk=roleid)
@classmethod @classmethod
def interface_for_roletype(cls, roletype): def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype""" """Return interfaces for a roletype"""
@ -1634,6 +1662,11 @@ class Role(RevMixin, AclMixin, models.Model):
machine__interface__role=cls.objects.filter(specific_role=roletype) machine__interface__role=cls.objects.filter(specific_role=roletype)
) )
@classmethod
def interface_for_roletype(cls, roletype):
"""Return interfaces for a roletype"""
return Interface.objects.filter(role=cls.objects.filter(specific_role=roletype))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(Role, self).save(*args, **kwargs) super(Role, self).save(*args, **kwargs)
@ -1847,7 +1880,7 @@ class OuverturePort(RevMixin, AclMixin, models.Model):
class Meta: class Meta:
verbose_name = _("ports opening") verbose_name = _("ports opening")
verbose_name = _("ports openings") verbose_name_plural = _("ports openings")
def __str__(self): def __str__(self):
if self.begin == self.end: if self.begin == self.end:
@ -2013,4 +2046,3 @@ def srv_post_save(**_kwargs):
def srv_post_delete(**_kwargs): def srv_post_delete(**_kwargs):
"""Regeneration dns après modification d'un SRV""" """Regeneration dns après modification d'un SRV"""
regen('dns') regen('dns')

View file

@ -26,8 +26,8 @@
Serializers for the Machines app Serializers for the Machines app
""" """
from rest_framework import serializers from rest_framework import serializers
from machines.models import ( from machines.models import (
Interface, Interface,
IpType, IpType,

View file

@ -45,4 +45,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -45,4 +45,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %} {% if ipv6_enabled %}
<th>{% trans "AAAA record origin" %}</th> <th>{% trans "AAAA record origin" %}</th>
{% endif %} {% endif %}
<th>{% trans "DNSSEC" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -50,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled %} {% if ipv6_enabled %}
<td>{{ extension.origin_v6 }}</td> <td>{{ extension.origin_v6 }}</td>
{% endif %} {% endif %}
<td>{{ extension.dnssec|tick }}</td>
<td class="text-right"> <td class="text-right">
{% 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 %}
@ -60,4 +62,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
</div> </div>

View file

@ -48,7 +48,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ type.type }}</td> <td>{{ type.type }}</td>
<td>{{ type.extension }}</td> <td>{{ type.extension }}</td>
<td>{{ type.need_infra|tick }}</td> <td>{{ type.need_infra|tick }}</td>
<td>{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}{% if type.ip_network %}<b><u> on </b></u>{{ type.ip_network }}{% endif %}</td> <td>{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}{% if type.ip_network %}<b><u> on </u></b>
{{ type.ip_network }}{% endif %}</td>
<td>{{ type.prefix_v6 }}/{{ type.prefix_v6_length }}</td> <td>{{ type.prefix_v6 }}/{{ type.prefix_v6_length }}</td>
<td>{{ type.reverse_v4|tick }}/{{ type.reverse_v6|tick }}</td> <td>{{ type.reverse_v4|tick }}/{{ type.reverse_v6|tick }}</td>
<td>{{ type.vlan }}</td> <td>{{ type.vlan }}</td>
@ -63,4 +64,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} {% endfor %}
</table> </table>
</div> </div>

View file

@ -50,4 +50,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="table-responsive"> <div class="table-responsive">
{% if machines_list.paginator %} {% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %} {% include 'pagination.html' with list=machines_list go_to_id="machines" %}
{% endif %} {% endif %}
<table class="table" id="machines_table"> <table class="table" id="machines_table">
@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</colgroup> </colgroup>
<thead> <thead>
{% trans "DNS name" as tr_dns_name %} {% trans "DNS name" as tr_dns_name %}
<th>{% include "buttons/sort.html" with prefix='machine' col='name' text=tr_dns_name %}</th> <th>{% include 'buttons/sort.html' with prefix='machine' col='name' text=tr_dns_name %}</th>
<th>{% trans "Type" %}</th> <th>{% trans "Type" %}</th>
<th>{% trans "MAC address" %}</th> <th>{% trans "MAC address" %}</th>
<th>{% trans "IP address" %}</th> <th>{% trans "IP address" %}</th>
@ -52,7 +52,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td colspan="4"> <td colspan="4">
{% trans "No name" as tr_no_name %} {% trans "No name" as tr_no_name %}
{% trans "View the profile" as tr_view_the_profile %} {% trans "View the profile" as tr_view_the_profile %}
<b>{{ machine.name|default:tr_no_name }}</b> <i class="fa-angle-right"></i> <b>{{ machine.get_name|default:tr_no_name }}</b> <i class="fa fa-angle-right"></i>
<a href="{% url 'users:profil' userid=machine.user.id %}" title=tr_view_the_profile> <a href="{% url 'users:profil' userid=machine.user.id %}" title=tr_view_the_profile>
<i class="fa fa-user"></i> {{ machine.user }} <i class="fa fa-user"></i> {{ machine.user }}
</a> </a>
@ -73,7 +73,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td> <td>
{% if interface.domain.related_domain.all %} {% if interface.domain.related_domain.all %}
{{ interface.domain }} {{ interface.domain }}
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#collapseDomain_{{ interface.id }}" aria-expanded="true" aria-controls="collapseDomain_{{ interface.id }}"> <button class="btn btn-default btn-xs" type="button" data-toggle="collapse"
data-target="#collapseDomain_{{ interface.id }}" aria-expanded="true"
aria-controls="collapseDomain_{{ interface.id }}">
{% trans "Display the aliases" %} {% trans "Display the aliases" %}
</button> </button>
{% else %} {% else %}
@ -91,7 +93,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<br> <br>
{% if ipv6_enabled and interface.ipv6 != 'None' %} {% if ipv6_enabled and interface.ipv6 != 'None' %}
<b>IPv6</b> <b>IPv6</b>
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#collapseIpv6_{{ interface.id }}" aria-expanded="true" aria-controls="collapseIpv6_{{ interface.id }}"> <button class="btn btn-default btn-xs" type="button" data-toggle="collapse"
data-target="#collapseIpv6_{{ interface.id }}" aria-expanded="true"
aria-controls="collapseIpv6_{{ interface.id }}">
{% trans "Display the IPv6 address" %} {% trans "Display the IPv6 address" %}
</button> </button>
{% endif %} {% endif %}
@ -99,7 +103,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td class="text-right"> <td class="text-right">
<div style="width: 128px;"> <div style="width: 128px;">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-primary btn-sm dropdown-toggle" type="button"
id="editioninterface" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fa fa-edit"></i> <span class="caret"></span> <i class="fa fa-edit"></i> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="editioninterface"> <ul class="dropdown-menu" aria-labelledby="editioninterface">
@ -156,7 +162,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td colspan=5 style="border-top: none; padding: 1px;"> <td colspan=5 style="border-top: none; padding: 1px;">
<div class="collapse in" id="collapseIpv6_{{ interface.id }}"> <div class="collapse in" id="collapseIpv6_{{ interface.id }}">
<ul class="list-group" style="margin-bottom: 0px;"> <ul class="list-group" style="margin-bottom: 0;">
{% for ipv6 in interface.ipv6.all %} {% for ipv6 in interface.ipv6.all %}
<li class="list-group-item col-xs-6 col-sm-6 col-md-6" style="border: none;"> <li class="list-group-item col-xs-6 col-sm-6 col-md-6" style="border: none;">
{{ ipv6 }} {{ ipv6 }}
@ -171,7 +177,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td colspan=5 style="border-top: none; padding: 1px;"> <td colspan=5 style="border-top: none; padding: 1px;">
<div class="collapse in" id="collapseDomain_{{ interface.id }}"> <div class="collapse in" id="collapseDomain_{{ interface.id }}">
<ul class="list-group" style="margin-bottom: 0px;"> <ul class="list-group" style="margin-bottom: 0;">
{% for al in interface.domain.related_domain.all %} {% for al in interface.domain.related_domain.all %}
<li class="list-group-item col-xs-6 col-sm-4 col-md-3" style="border: none;"> <li class="list-group-item col-xs-6 col-sm-4 col-md-3" style="border: none;">
<a href="http://{{ al }}"> <a href="http://{{ al }}">
@ -191,7 +197,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</thead>
</table> </table>
<script> <script>
@ -210,7 +215,6 @@ $("#machines_table").ready( function() {
</script> </script>
{% if machines_list.paginator %} {% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %} {% include 'pagination.html' with list=machines_list go_to_id="machines" %}
{% endif %} {% endif %}
</div> </div>

View file

@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -49,4 +49,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -54,4 +54,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -51,4 +51,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

Some files were not shown because too many files have changed in this diff Show more