8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-23 11:53:12 +00:00

Merge branch 'dev' into '2.7'

Dev

See merge request federez/re2o!398
This commit is contained in:
klafyvel 2019-01-23 00:08:39 +01:00
commit beae8ee83e
202 changed files with 5814 additions and 2608 deletions

View file

@ -164,3 +164,17 @@ 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

@ -28,7 +28,7 @@ done.
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
def _create_api_permission():
@ -71,4 +71,5 @@ def can_view(user):
'codename': settings.API_PERMISSION_CODENAME
}
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

@ -46,6 +46,6 @@ class ExpiringTokenAuthentication(TokenAuthentication):
)
utc_now = datetime.datetime.now(datetime.timezone.utc)
if token.created < utc_now - token_duration:
raise exceptions.AuthenticationFailed(_('Token has expired'))
raise exceptions.AuthenticationFailed(_("The token has expired."))
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

@ -391,13 +391,25 @@ class OptionalTopologieSerializer(NamespacedHMSerializer):
class Meta:
model = preferences.OptionalTopologie
fields = ('radius_general_policy', 'vlan_decision_ok',
'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management',
fields = ('switchs_ip_type', 'switchs_web_management',
'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):
"""Serialize `preferences.models.GeneralOption` objects.
"""
@ -811,7 +823,8 @@ class SwitchPortSerializer(serializers.ModelSerializer):
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')
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value',
'list_modules')
# LOCAL EMAILS

View file

@ -67,6 +67,7 @@ router.register_viewset(r'machines/role', views.RoleViewSet)
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
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_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'),
router.register_view(r'preferences/assooption', views.AssoOptionView),

View file

@ -292,6 +292,17 @@ class OptionalTopologieView(generics.RetrieveAPIView):
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):
"""Exposes details of `preferences.models.GeneralOption` settings.
"""
@ -600,9 +611,11 @@ class HostMacIpView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = all_active_interfaces()
serializer_class = serializers.HostMacIpSerializer
def get_queryset(self):
return all_active_interfaces()
# Firewall
@ -635,7 +648,7 @@ class DNSZonesView(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.
"""
queryset = (machines.IpType.objects.all())

View file

@ -30,7 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice
from .models import CustomInvoice, CostEstimate
class FactureAdmin(VersionAdmin):
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass
class CostEstimateAdmin(VersionAdmin):
"""Admin class for cost estimates."""
pass
class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices."""
pass
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin)
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.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
@ -104,7 +107,44 @@ class SelectArticleForm(FormRevMixin, Form):
user = kwargs.pop('user')
target_user = kwargs.pop('target_user', None)
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):
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
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):
"""
Form used to create an article.
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
super(RechargeForm, self).__init__(*args, **kwargs)
self.fields['payment'].empty_label = \
_("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):
"""
@ -260,10 +310,9 @@ class RechargeForm(FormRevMixin, Form):
if balance_method.maximum_balance is not None and \
value + self.user.solde > balance_method.maximum_balance:
raise forms.ValidationError(
_("Requested amount is too high. Your balance can't exceed \
%(max_online_balance)s .") % {
_("Requested amount is too high. Your balance can't exceed"
" %(max_online_balance)s €.") % {
'max_online_balance': balance_method.maximum_balance
}
)
return self.cleaned_data

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\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"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language: fr_FR\n"
@ -33,79 +33,98 @@ msgstr ""
msgid "You don't have the right to view this 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"
msgstr "Sélectionnez un moyen de paiement"
#: forms.py:66 models.py:510
#: forms.py:69 models.py:579
msgid "Member"
msgstr "Adhérent"
#: forms.py:68
#: forms.py:71
msgid "Select the proprietary member"
msgstr "Sélectionnez l'adhérent propriétaire"
#: forms.py:69
#: forms.py:72
msgid "Validated invoice"
msgstr "Facture validée"
#: forms.py:82
#: forms.py:85
msgid "A payment method must be specified."
msgstr "Un moyen de paiement doit être renseigné."
#: forms.py:96 forms.py:120 templates/cotisations/aff_article.html:33
#: templates/cotisations/facture.html:61
#: forms.py:97 templates/cotisations/aff_article.html:33
#: templates/cotisations/facture.html:67
msgid "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"
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"
msgstr "Nom de l'article"
#: forms.py:164 templates/cotisations/sidebar.html:50
#: forms.py:189 templates/cotisations/sidebar.html:55
msgid "Available articles"
msgstr "Articles disponibles"
#: forms.py:192
#: forms.py:217
msgid "Payment method name"
msgstr "Nom du moyen de paiement"
#: forms.py:204
#: forms.py:229
msgid "Available payment methods"
msgstr "Moyens de paiement disponibles"
#: forms.py:230
#: forms.py:255
msgid "Bank name"
msgstr "Nom de la banque"
#: forms.py:242
#: forms.py:267
msgid "Available banks"
msgstr "Banques disponibles"
#: forms.py:261
#: forms.py:286
msgid "Amount"
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/control.html:66
msgid "Payment method"
msgstr "Moyen de paiement"
#: forms.py:287
#: forms.py:313
#, python-format
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 €."
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 €."
#: 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/control.html:70
msgid "Date"
@ -133,9 +152,9 @@ msgstr "Peut voir un objet facture"
#: models.py:158
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"
msgstr "facture"
@ -156,128 +175,149 @@ msgid ""
"You don't have the right to edit an invoice already controlled or "
"invalidated."
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."
#: models.py:184
msgid "You don't have the right to delete an invoice."
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."
msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur."
#: models.py:189
#: models.py:191
msgid ""
"You don't have the right to delete an invoice already controlled or "
"invalidated."
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."
#: models.py:197
#: models.py:199
msgid "You don't have the right to view someone else's invoices history."
msgstr ""
"Vous n'avez pas le droit de voir l'historique des factures d'un autre "
"utilisateur."
#: models.py:200
#: models.py:202
msgid "The invoice has been invalidated."
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."
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."
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."
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"
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"
msgstr "Destinataire"
#: models.py:269 templates/cotisations/aff_paiement.html:33
#: models.py:280 templates/cotisations/aff_paiement.html:33
msgid "Payment type"
msgstr "Type de paiement"
#: models.py:273
#: models.py:284
msgid "Address"
msgstr "Adresse"
#: models.py:276 templates/cotisations/aff_custom_invoice.html:54
#: models.py:287 templates/cotisations/aff_custom_invoice.html:54
msgid "Paid"
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"
msgstr "Connexion"
#: models.py:297 models.py:517 models.py:765
#: models.py:365 models.py:586 models.py:853
msgid "Membership"
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"
msgstr "Les deux"
#: models.py:310
#: models.py:378
msgid "amount"
msgstr "montant"
#: models.py:315
#: models.py:383
msgid "article"
msgstr "article"
#: models.py:322
#: models.py:390
msgid "price"
msgstr "prix"
#: models.py:327 models.py:535
#: models.py:395 models.py:604
msgid "duration (in months)"
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"
msgstr "type de cotisation"
#: models.py:340
#: models.py:408
msgid "Can view a purchase object"
msgstr "Peut voir un objet achat"
#: models.py:341
#: models.py:409
msgid "Can edit all the previous purchases"
msgstr "Peut modifier tous les achats précédents"
#: models.py:343 models.py:774
#: models.py:411 models.py:862
msgid "purchase"
msgstr "achat"
#: models.py:344
#: models.py:412
msgid "purchases"
msgstr "achats"
#: models.py:411 models.py:573
#: models.py:479 models.py:642
msgid "Duration must be specified for a subscription."
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."
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."
msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur."
#: models.py:427
#: models.py:495
msgid ""
"You don't have the right to edit a purchase already controlled or "
"invalidated."
@ -285,150 +325,150 @@ msgstr ""
"Vous n'avez pas le droit de modifier un achat précédemment contrôlé ou "
"invalidé."
#: models.py:434
#: models.py:502
msgid "You don't have the right to delete a purchase."
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."
msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur."
#: models.py:439
#: models.py:507
msgid ""
"You don't have the right to delete a purchase already controlled or "
"invalidated."
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é."
#: models.py:447
#: models.py:515
msgid "You don't have the right to view someone else's purchase history."
msgstr ""
"Vous n'avez pas le droit de voir l'historique des achats d'un autre "
"utilisateur."
#: models.py:511
#: models.py:580
msgid "Club"
msgstr "Club"
#: models.py:523
#: models.py:592
msgid "designation"
msgstr "désignation"
#: models.py:529
#: models.py:598
msgid "unit price"
msgstr "prix unitaire"
#: models.py:541
#: models.py:610
msgid "type of users concerned"
msgstr "type d'utilisateurs concernés"
#: models.py:553 models.py:649
#: models.py:622 models.py:733
msgid "is available for every user"
msgstr "est disponible pour chaque utilisateur"
#: models.py:560
#: models.py:629
msgid "Can view an article object"
msgstr "Peut voir un objet article"
#: models.py:561
#: models.py:630
msgid "Can buy every article"
msgstr "Peut acheter chaque article"
#: models.py:569
#: models.py:638
msgid "Balance is a reserved article name."
msgstr "Solde est un nom d'article réservé."
#: models.py:594
#: models.py:663
msgid "You can't buy this article."
msgstr "Vous ne pouvez pas acheter cet article."
#: models.py:624
#: models.py:708
msgid "Can view a bank object"
msgstr "Peut voir un objet banque"
#: models.py:626
#: models.py:710
msgid "bank"
msgstr "banque"
#: models.py:627
#: models.py:711
msgid "banks"
msgstr "banques"
#: models.py:645
#: models.py:729
msgid "method"
msgstr "moyen"
#: models.py:654
#: models.py:738
msgid "is user balance"
msgstr "est solde utilisateur"
#: models.py:655
#: models.py:739
msgid "There should be only one balance payment method."
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"
msgstr "Peut voir un objet moyen de paiement"
#: models.py:662
#: models.py:746
msgid "Can use every payment method"
msgstr "Peut utiliser chaque moyen de paiement"
#: models.py:664
#: models.py:748
msgid "payment method"
msgstr "moyen de paiement"
#: models.py:665
#: models.py:749
msgid "payment methods"
msgstr "moyens de paiement"
#: models.py:699 payment_methods/comnpay/views.py:63
#: models.py:787 payment_methods/comnpay/views.py:63
#, python-format
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."
#: models.py:709
#: models.py:797
msgid "The invoice was created."
msgstr "La facture a été créée."
#: models.py:730
#: models.py:818
msgid "You can't use this payment method."
msgstr "Vous ne pouvez pas utiliser ce moyen de paiement."
#: models.py:748
#: models.py:836
msgid "No custom payment method."
msgstr "Pas de moyen de paiement personnalisé."
#: models.py:783
#: models.py:871
msgid "start date"
msgstr "date de début"
#: models.py:786
#: models.py:874
msgid "end date"
msgstr "date de fin"
#: models.py:791
#: models.py:879
msgid "Can view a subscription object"
msgstr "Peut voir un objet cotisation"
#: models.py:792
#: models.py:880
msgid "Can edit the previous subscriptions"
msgstr "Peut modifier les cotisations précédentes"
#: models.py:794
#: models.py:882
msgid "subscription"
msgstr "cotisation"
#: models.py:795
#: models.py:883
msgid "subscriptions"
msgstr "cotisations"
#: models.py:799
#: models.py:887
msgid "You don't have the right to edit a subscription."
msgstr "Vous n'avez pas le droit de modifier une cotisation."
#: models.py:803
#: models.py:891
msgid ""
"You don't have the right to edit a subscription already controlled or "
"invalidated."
@ -436,11 +476,11 @@ msgstr ""
"Vous n'avez pas le droit de modifier une cotisation précédemment contrôlée "
"ou invalidée."
#: models.py:810
#: models.py:898
msgid "You don't have the right to delete a subscription."
msgstr "Vous n'avez pas le droit de supprimer une cotisation."
#: models.py:813
#: models.py:901
msgid ""
"You don't have the right to delete a subscription already controlled or "
"invalidated."
@ -448,7 +488,7 @@ msgstr ""
"Vous n'avez pas le droit de supprimer une cotisation précédemment contrôlée "
"ou invalidée."
#: models.py:821
#: models.py:909
msgid "You don't have the right to view someone else's subscription history."
msgstr ""
"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"
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."
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."
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)"
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 "
msgstr "Payer la facture numéro "
#: payment_methods/comnpay/models.py:116
#: payment_methods/comnpay/models.py:114
msgid ""
"In order to pay your invoice with ComNpay, the price must be greater than {} "
"€."
@ -559,6 +599,30 @@ msgstr ""
msgid "no"
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
msgid "Price"
msgstr "Prix"
@ -579,34 +643,47 @@ msgstr "Utilisateurs concernés"
msgid "Available for everyone"
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
msgid "Bank"
msgstr "Banque"
#: templates/cotisations/aff_cotisations.html:38
msgid "User"
msgstr "Utilisateur"
#: templates/cotisations/aff_cost_estimate.html:39
#: templates/cotisations/aff_cotisations.html:41
#: templates/cotisations/aff_custom_invoice.html:39
#: templates/cotisations/control.html:63
#: templates/cotisations/edit_facture.html:45
#: templates/cotisations/edit_facture.html:49
msgid "Designation"
msgstr "Désignation"
#: templates/cotisations/aff_cost_estimate.html:40
#: templates/cotisations/aff_cotisations.html:42
#: templates/cotisations/aff_custom_invoice.html:40
#: templates/cotisations/control.html:64
msgid "Total price"
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_custom_invoice.html:50
#: templates/cotisations/control.html:56
@ -617,11 +694,6 @@ msgstr "ID facture"
msgid "Controlled invoice"
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
msgid "Invalidated invoice"
msgstr "Facture invalidée"
@ -666,6 +738,11 @@ msgstr "Validé"
msgid "Controlled"
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
msgid "Deletion of subscriptions"
msgstr "Suppression de cotisations"
@ -676,33 +753,33 @@ msgid ""
"Warning: are you sure you really want to delete this %(object_name)s object "
"( %(objet)s )?"
msgstr ""
"\tAttention: voulez-vous vraiment supprimer cet objet %(object_name)s "
"Attention: voulez-vous vraiment supprimer cet objet %(object_name)s "
"( %(objet)s ) ?"
#: templates/cotisations/delete.html:38
#: templates/cotisations/edit_facture.html:60
#: views.py:181 views.py:235
#: templates/cotisations/edit_facture.html:64 views.py:178 views.py:228
#: views.py:280
msgid "Confirm"
msgstr "Valider"
msgstr "Confirmer"
#: templates/cotisations/edit_facture.html:31
#: templates/cotisations/facture.html:30
msgid "Creation and editing of invoices"
msgstr "Création et modification de factures"
#: templates/cotisations/edit_facture.html:38
msgid "Edit the invoice"
#: templates/cotisations/edit_facture.html:41
msgid "Edit invoice"
msgstr "Modifier la facture"
#: templates/cotisations/edit_facture.html:41
#: templates/cotisations/facture.html:56
#: templates/cotisations/edit_facture.html:45
#: templates/cotisations/facture.html:62
#: templates/cotisations/index_article.html:30
msgid "Articles"
msgstr "Articles"
#: templates/cotisations/facture.html:37
msgid "Buy"
msgstr "Acheter une cotisation"
msgstr "Acheter"
#: templates/cotisations/facture.html:40
#, python-format
@ -714,11 +791,11 @@ msgstr "Solde maximum autorisé : %(max_balance)s €"
msgid "Current balance: %(balance)s €"
msgstr "Solde actuel : %(balance)s €"
#: templates/cotisations/facture.html:70
#: templates/cotisations/facture.html:76
msgid "Add an extra 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> €"
msgstr "Prix total : <span id=\"total_price\">0,00</span> €"
@ -730,9 +807,8 @@ msgstr "Factures"
msgid "Subscriptions"
msgstr "Cotisations"
#: templates/cotisations/index_article.html:33
msgid "Article types list"
msgid "List of article types"
msgstr "Liste des types d'article"
#: templates/cotisations/index_article.html:36
@ -744,12 +820,12 @@ msgid "Delete one or several article types"
msgstr "Supprimer un ou plusieurs types d'article"
#: templates/cotisations/index_banque.html:30
#: templates/cotisations/sidebar.html:55
#: templates/cotisations/sidebar.html:60
msgid "Banks"
msgstr "Banques"
#: templates/cotisations/index_banque.html:33
msgid "Banks list"
msgid "List of banks"
msgstr "Liste des banques"
#: templates/cotisations/index_banque.html:36
@ -760,17 +836,26 @@ msgstr "Ajouter une banque"
msgid "Delete one or several banks"
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/sidebar.html:45
msgid "Custom invoices"
msgstr "Factures personnalisées"
#: templates/cotisations/index_custom_invoice.html:31
msgid "Custom invoices list"
msgstr "Liste des factures personalisées"
msgid "List of custom invoices"
msgstr "Liste des factures personnalisées"
#: templates/cotisations/index_paiement.html:30
#: templates/cotisations/sidebar.html:60
#: templates/cotisations/sidebar.html:65
msgid "Payment methods"
msgstr "Moyens de paiement"
@ -793,9 +878,9 @@ msgstr "Rechargement de solde"
#: templates/cotisations/payment.html:34
#, python-format
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"
msgstr "Payer"
@ -807,81 +892,104 @@ msgstr "Créer une facture"
msgid "Control the invoices"
msgstr "Contrôler les factures"
#: views.py:167
#: views.py:164
msgid "You need to choose at least one article."
msgstr "Vous devez choisir au moins un article."
#: views.py:222
msgid "The cost estimate was created."
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."
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."
msgstr "La facture a été modifiée."
#: views.py:336 views.py:430
#: views.py:383 views.py:589
msgid "The invoice was deleted."
msgstr "La facture a été supprimée."
#: views.py:341 views.py:435
#: views.py:388 views.py:594
msgid "Invoice"
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."
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"
msgstr "Ajouter"
#: views.py:462
#: views.py:621
msgid "New article"
msgstr "Nouvel article"
#: views.py:478
#: views.py:637
msgid "The article was edited."
msgstr "L'article a été modifié."
#: views.py:484
#: views.py:643
msgid "Edit article"
msgstr "Modifier l'article"
#: views.py:500
#: views.py:659
msgid "The articles were deleted."
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"
msgstr "Supprimer"
#: views.py:506
#: views.py:665
msgid "Delete article"
msgstr "Supprimer l'article"
#: views.py:528
#: views.py:687
msgid "The payment method was created."
msgstr "Le moyen de paiment a été créé."
#: views.py:535
#: views.py:694
msgid "New payment method"
msgstr "Nouveau moyen de paiement"
#: views.py:564
#: views.py:723
msgid "The payment method was edited."
msgstr "Le moyen de paiment a été modifié."
#: views.py:571
#: views.py:730
msgid "Edit payment method"
msgstr "Modifier le moyen de paiement"
#: views.py:590
#: views.py:749
#, python-format
msgid "The payment method %(method_name)s was deleted."
msgstr "Le moyen de paiement %(method_name)s a été supprimé."
#: views.py:597
#: views.py:756
#, python-format
msgid ""
"The payment method %(method_name)s can't be deleted "
@ -890,52 +998,51 @@ msgstr ""
"Le moyen de paiement %(method_name)s ne peut pas être supprimé car il y a "
"des factures qui l'utilisent."
#: views.py:606
#: views.py:765
msgid "Delete payment method"
msgstr "Supprimer le moyen de paiement"
#: views.py:622
#: views.py:781
msgid "The bank was created."
msgstr "La banque a été créée."
#: views.py:628
#: views.py:787
msgid "New bank"
msgstr "Nouvelle banque"
#: views.py:645
#: views.py:804
msgid "The bank was edited."
msgstr "La banque a été modifiée."
#: views.py:651
#: views.py:810
msgid "Edit bank"
msgstr "Modifier la banque"
#: views.py:670
#: views.py:829
#, python-format
msgid "The bank %(bank_name)s was deleted."
msgstr "La banque %(bank_name)s a été supprimée."
#: views.py:677
#: views.py:836
#, python-format
msgid ""
"The bank %(bank_name)s can't be deleted because there "
"are invoices using it."
"The bank %(bank_name)s can't be deleted because there are invoices using it."
msgstr ""
"La banque %(bank_name)s ne peut pas être supprimée car il y a des factures "
"qui l'utilisent."
#: views.py:686
#: views.py:845
msgid "Delete bank"
msgstr "Supprimer la banque"
#: views.py:722
#: views.py:881
msgid "Your changes have been properly taken into account."
msgstr "Vos modifications ont correctement été prises en compte."
#: views.py:834
#: views.py:1016
msgid "You are not allowed to credit your balance."
msgstr "Vous n'êtes pas autorisés à créditer votre solde."
#: views.py:869
#: views.py:1048
msgid "Refill your balance"
msgstr "Recharger votre solde"

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.contrib import messages
from preferences.models import CotisationsOption
from machines.models import regen
from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin
from cotisations.utils import find_payment_method, send_mail_invoice
from cotisations.utils import (
find_payment_method, send_mail_invoice, send_mail_voucher
)
from cotisations.validators import check_no_balance
@ -236,15 +239,35 @@ class Facture(BaseInvoice):
'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):
return str(self.user) + ' ' + str(self.date)
@receiver(post_save, sender=Facture)
def facture_post_save(**kwargs):
"""
@ -284,8 +307,65 @@ class CustomInvoice(BaseInvoice):
verbose_name=_("Address")
)
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
@ -624,7 +704,7 @@ class Article(RevMixin, AclMixin, models.Model):
objects_pool = cls.objects.filter(
Q(type_user='All') | Q(type_user='Adherent')
)
if not target_user.is_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')
)
@ -718,7 +798,7 @@ class Paiement(RevMixin, AclMixin, models.Model):
if payment_method is not None and use_payment_method:
return payment_method.end_payment(invoice, request)
## So make this invoice valid, trigger send mail
# So make this invoice valid, trigger send mail
invoice.valid = True
invoice.save()

View file

@ -57,7 +57,7 @@ def note_payment(request, facture, factureid):
user = facture.user
payment_method = find_payment_method(facture.paiement)
if not payment_method or not isinstance(payment_method, NotePayment):
messages.error(request, "Erreur inconnue")
messages.error(request, _("Unknown error."))
return redirect(reverse(
'users:profil',
kwargs={'userid': user.id}
@ -85,7 +85,7 @@ def note_payment(request, facture, factureid):
)
facture.valid = True
facture.save()
messages.success(request, "Le paiement par note a bien été effectué")
messages.success(request, _("The payment with note was done."))
return redirect(reverse(
'users:profil',
kwargs={'userid': user.id}

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 class="text-right">
{% can_edit article %}
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}">
<i class="fa fa-edit"></i>
</a>
{% include 'buttons/edit.html' with href='cotisations:edit-article' id=article.id %}
{% acl_end %}
{% history_button article %}
</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

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="table-responsive">
{% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %}
{% include 'pagination.html' with list=facture_list %}
{% endif %}
<table class="table table-striped">
@ -48,7 +48,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %}
</th>
<th>
<th>
{% trans "Invoice ID" as tr_invoice_id %}
{% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %}
</th>
@ -63,17 +63,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ facture.prix_total }}</td>
<td>{{ facture.paiement }}</td>
<td>{{ facture.date }}</td>
<td>{{ facture.id }}</td>
<td>{{ facture.id }}</td>
<td>
{% can_edit facture %}
{% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %}
{% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %}
{% acl_else %}
{% trans "Controlled invoice" %}
{% acl_end %}
{% can_delete facture %}
{% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %}
{% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %}
{% acl_end %}
{% history_button facture %}
{% history_button facture %}
</td>
<td>
{% if facture.valid %}
@ -83,13 +83,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% else %}
<i class="text-danger">{% trans "Invalidated invoice" %}</i>
{% 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>
</tr>
{% endfor %}
</table>
{% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %}
{% endif %}
{% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %}
{% endif %}
</div>

View file

@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="table-responsive">
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
<table class="table table-striped">
@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr>
<th>
{% 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>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %}
</th>
<th>
{% trans "Paid" as tr_invoice_paid%}
{% trans "Paid" as tr_invoice_paid %}
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_paid %}
</th>
<th></th>
@ -84,6 +84,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -45,9 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td>
<td class="text-right">
{% can_edit paiement %}
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}">
<i class="fa fa-edit"></i>
</a>
{% include 'buttons/edit.html' with href='cotisations:edit-paiement' id=paiement.id %}
{% acl_end %}
{% history_button paiement %}
</td>

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %}
{% 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
@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h2>{% trans "Invoice control and validation" %}</h2>
{% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %}
{% include 'pagination.html' with list=facture_list %}
{% endif %}
<form class="form" method="post">
@ -110,6 +110,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endblock %}
{% if facture_list.paginator %}
{% include 'pagination.html' with list=facture_list %}
{% include 'pagination.html' with list=facture_list %}
{% endif %}

View file

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

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %}
{% 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
@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post">
{% 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' %}
{{ venteform.management_form }}
<h3>{% trans "Articles" %}</h3>

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 %}
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
@ -44,6 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
</p>
{% endif %}
{% if factureform %}
{% bootstrap_form_errors factureform %}
{% endif %}
{% if discount_form %}
{% bootstrap_form_errors discount_form %}
{% endif %}
<form class="form" method="post">
{% csrf_token %}
@ -68,8 +74,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %}
</div>
<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>
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
</p>
{% endif %}
{% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
@ -78,105 +88,117 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if articlesformset or payment_method%}
<script type="text/javascript">
{% if articlesformset %}
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var prices = {};
{% for article in articlelist %}
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var template = `Article : &nbsp;
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span>
</button>`
var template = `Article : &nbsp;
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span>
</button>`
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
function update_price(){
var price = 0;
var product_count =
document.getElementsByClassName('product_to_sell').length;
var article, article_price, quantity;
for (i = 0; i < product_count; ++i){
article = document.getElementById(
'id_form-' + i.toString() + '-article').value;
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
if (article == '') {
continue;
}
article_price = prices[article];
quantity = document.getElementById(
'id_form-' + i.toString() + '-quantity').value;
price += article_price * quantity;
}
document.getElementById('total_price').innerHTML =
price.toFixed(2).toString().replace('.', ',');
price += article_price * quantity;
}
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
})
{% 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 =
price.toFixed(2).toString().replace('.', ',');
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
update_price();
});
function add_listenner_for_id(i){
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article')
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) {
var article = event.target.parentNode;
article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --;
update_price();
})
}
// Add events manager when DOM is fully loaded
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one")
.addEventListener("click", add_article, true);
var product_count =
document.getElementsByClassName('product_to_sell').length;
for (i = 0; i < product_count; ++i){
add_listenner_for_id(i);
}
document.getElementById('id_discount')
.addEventListener('change', update_price, true);
document.getElementById('id_is_relative')
.addEventListener('click', update_price, true);
update_price();
});
{% endif %}
{% if payment_method.templates %}
var TEMPLATES = [
"",
{% for t in payment_method.templates %}
{% if t %}
`{% bootstrap_form t %}`,
{% else %}
"",
{% endif %}
{% endfor %}
];
function update_payment_method_form(){
var method = document.getElementById('paymentMethodSelect').value;
if(method==""){
method=0;
}
else{
method = Number(method);
method += 1;
}
console.log(method);
var html = TEMPLATES[method];
document.getElementById('paymentMethod').innerHTML = html;
var TEMPLATES = [
"",
{% for t in payment_method.templates %}
{% if t %}
`{% bootstrap_form t %}`,
{% else %}
"",
{% endif %}
{% endfor %}
];
function update_payment_method_form(){
var method = document.getElementById('paymentMethodSelect').value;
if(method==""){
method=0;
}
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
else{
method = Number(method);
method += 1;
}
console.log(method);
var html = TEMPLATES[method];
document.getElementById('paymentMethod').innerHTML = html;
}
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
{% endif %}
</script>
{% endif %}

View file

@ -43,7 +43,7 @@
\begin{document}
%----------------------------------------------------------------------------------------
% HEADING SECTION
%----------------------------------------------------------------------------------------
@ -70,13 +70,17 @@
{\bf Siret :} {{siret|safe}}
\vspace{2cm}
\begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r}
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
{% if fid is not None %}
{% if is_estimate %}
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
{% else %}
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
{% endif %}
{% endif %}
\end{tabular*}
\\
@ -84,39 +88,57 @@
%----------------------------------------------------------------------------------------
% TABLE OF EXPENSES
%----------------------------------------------------------------------------------------
\begin{tabularx}{\textwidth}{|X|r|r|r|}
\hline
\textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\
\doublehline
{% for a in article %}
{{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\
\hline
{% endfor %}
\end{tabularx}
\vspace{1cm}
\hfill
\begin{tabular}{|l|r|}
\hline
\textbf{Total} & {{total|floatformat:2}} \euro \\
{% if not is_estimate %}
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
\doublehline
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
{% endif %}
\hline
\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
%----------------------------------------------------------------------------------------
% FOOTNOTE
%----------------------------------------------------------------------------------------
\hrule
\smallskip
\footnotesize{TVA non applicable, art. 293 B du CGI}

View file

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

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %}
{% 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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Articles" %}{% endblock %}
{% block content %}
<h2>{% trans "Article types list" %}</h2>
<h2>{% trans "List of article types" %}</h2>
{% can_create 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" %}

View file

@ -1,4 +1,4 @@
{% extends "cotisations/sidebar.html" %}
{% 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
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Banks" %}{% endblock %}
{% block content %}
<h2>{% trans "Banks list" %}</h2>
<h2>{% trans "List of banks" %}</h2>
{% can_create 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" %}

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 %}
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
@ -28,9 +28,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "Custom invoices" %}{% endblock %}
{% block content %}
<h2>{% trans "Custom invoices list" %}</h2>
<h2>{% trans "List of custom invoices" %}</h2>
{% 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 %}
{% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %}
{% endblock %}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends 'base.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
@ -28,35 +28,40 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block sidebar %}
{% 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" %}
</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" %}
</a>
{% acl_end %}
{% 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" %}
</a>
{% acl_end %}
{% 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" %}
</a>
{% 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 %}
<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" %}
</a>
{% acl_end %}
{% 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" %}
</a>
{% acl_end %}
{% 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" %}
</a>
{% 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

@ -31,11 +31,16 @@ from subprocess import Popen, PIPE
import os
from datetime import datetime
from django.db import models
from django.template.loader import get_template
from django.template import Context
from django.http import HttpResponse
from django.conf import settings
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-')
@ -48,15 +53,40 @@ def render_invoice(_request, ctx={}):
Render an invoice using some available information such as the current
date, the user, the articles, the prices, ...
"""
options, _ = CotisationsOption.objects.get_or_create()
is_estimate = ctx.get('is_estimate', False)
filename = '_'.join([
'invoice',
'cost_estimate' if is_estimate else 'invoice',
slugify(ctx.get('asso_name', "")),
slugify(ctx.get('recipient_name', "")),
str(ctx.get('DATE', datetime.now()).year),
str(ctx.get('DATE', datetime.now()).month),
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(
name=filename
)
@ -81,18 +111,33 @@ def create_pdf(template, ctx={}):
with tempfile.TemporaryDirectory() as tempdir:
for _ in range(2):
process = Popen(
['pdflatex', '-output-directory', tempdir],
stdin=PIPE,
stdout=PIPE,
)
process.communicate(rendered_tpl)
with open("/var/www/re2o/out.log", "w") as f:
process = Popen(
['pdflatex', '-output-directory', tempdir],
stdin=PIPE,
stdout=f,#PIPE,
)
process.communicate(rendered_tpl)
with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f:
pdf = f.read()
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={}):
"""Creates a PDF from a LaTex templates using pdflatex.

View file

@ -51,11 +51,46 @@ urlpatterns = [
views.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(
r'^index_custom_invoice/$',
views.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(
r'^new_custom_invoice/$',
views.new_custom_invoice,
@ -146,5 +181,5 @@ urlpatterns = [
views.control,
name='control'
),
url(r'^$', views.index, name='index'),
url(r'^$', views.index, name='index'),
] + payment_methods.urls.urlpatterns

View file

@ -25,7 +25,7 @@ from django.template.loader import get_template
from django.core.mail import EmailMessage
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 import settings
@ -93,3 +93,38 @@ def send_mail_invoice(invoice):
attachments=[('invoice.pdf', pdf, 'application/pdf')]
)
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

@ -68,7 +68,8 @@ from .models import (
Paiement,
Banque,
CustomInvoice,
BaseInvoice
BaseInvoice,
CostEstimate,
)
from .forms import (
FactureForm,
@ -80,9 +81,11 @@ from .forms import (
DelBanqueForm,
SelectArticleForm,
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 .utils import find_payment_method
@ -178,7 +181,59 @@ def new_facture(request, user, userid):
)
# 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
@can_create(CustomInvoice)
def new_custom_invoice(request):
@ -198,8 +253,9 @@ def new_custom_invoice(request):
request.POST or None,
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()
for art_item in articles_formset:
if art_item.cleaned_data:
@ -213,6 +269,7 @@ def new_custom_invoice(request):
duration=article.duration,
number=quantity
)
discount_form.apply_to_invoice(new_invoice_instance)
messages.success(
request,
_("The custom invoice was created.")
@ -223,7 +280,8 @@ def new_custom_invoice(request):
'factureform': invoice_form,
'action_name': _("Confirm"),
'articlesformset': articles_formset,
'articlelist': articles
'articlelist': articles,
'discount_form': discount_form
}, 'cotisations/facture.html', request)
@ -266,7 +324,8 @@ def facture_pdf(request, facture, **_kwargs):
'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)
'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)
@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
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
@ -367,22 +475,21 @@ def edit_custom_invoice(request, invoice, **kwargs):
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
@can_view(CostEstimate)
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
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': purchase.name,
'name': escape_chars(purchase.name),
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
@ -401,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
'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)
'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
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
@ -550,7 +720,7 @@ def edit_paiement(request, paiement_instance, **_kwargs):
if payment_method is not None:
payment_method.save()
messages.success(
request,_("The payment method was edited.")
request, _("The payment method was edited.")
)
return redirect(reverse('cotisations:index-paiement'))
return form({
@ -663,8 +833,8 @@ def del_banque(request, instances):
except ProtectedError:
messages.error(
request,
_("The bank %(bank_name)s can't be deleted \
because there are invoices using it.") % {
_("The bank %(bank_name)s can't be deleted because there"
" are invoices using it.") % {
'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
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
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,
request.GET.get('col'),
@ -827,7 +1021,8 @@ def credit_solde(request, user, **_kwargs):
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():
price = refill_form.cleaned_data['value']
invoice = Facture(user=user)
@ -857,3 +1052,29 @@ def credit_solde(request, user, **_kwargs):
'max_balance': find_payment_method(p).maximum_balance,
}, '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

@ -289,7 +289,7 @@ def check_user_machine_and_register(nas_type, username, mac_address):
Renvoie le mot de passe ntlm de l'user si tout est ok
Utilise pour les authentifications en 802.1X"""
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:
return (False, u"User inconnu", '')
if not user.has_access():

View file

@ -59,7 +59,7 @@ _ask_value() {
install_requirements() {
### Usage: install_requirements
### Usage: install_requirements
#
# This function will install the required packages from APT repository
# and Pypi repository. Those packages are all required for Re2o to work
@ -273,7 +273,7 @@ write_settings_file() {
django_secret_key="$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(50)]))")"
aes_key="$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(32)]))")"
if [ "$db_engine_type" == 1 ]; then
sed -i 's/db_engine/django.db.backends.mysql/g' "$SETTINGS_LOCAL_FILE"
else
@ -324,6 +324,21 @@ update_django() {
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"
}
create_superuser() {
### Usage: create_superuser
#
@ -476,7 +491,7 @@ interactive_guide() {
sql_host="$(dialog --clear --backtitle "$BACKTITLE" \
--title "$TITLE" --inputbox "$INPUTBOX" \
$HEIGHT $WIDTH 2>&1 >/dev/tty)"
# Prompt to enter the remote database name
TITLE="SQL database name"
INPUTBOX="The name of the remote SQL database"
@ -523,14 +538,14 @@ interactive_guide() {
ldap_is_local="$(dialog --clear --backtitle "$BACKTITLE" \
--title "$TITLE" --menu "$MENU" \
$HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)"
# Prompt to enter the LDAP domain extension
TITLE="Domain extension"
INPUTBOX="The local domain extension to use (e.g. 'example.net'). This is used in the LDAP configuration."
extension_locale="$(dialog --clear --backtitle "$BACKTITLE" \
--title "$TITLE" --inputbox "$INPUTBOX" \
$HEIGHT $WIDTH 2>&1 >/dev/tty)"
# Building the DN of the LDAP from the extension
IFS='.' read -a extension_locale_array <<< $extension_locale
for i in "${extension_locale_array[@]}"
@ -546,7 +561,7 @@ interactive_guide() {
ldap_host="$(dialog --clear --backtitle "$BACKTITLE" \
--title "$TITLE" --inputbox "$INPUTBOX" \
$HEIGHT $WIDTH 2>&1 >/dev/tty)"
# Prompt to choose if TLS should be activated or not for the LDAP
TITLE="TLS on LDAP"
MENU="Would you like to activate TLS for communicating with the remote LDAP ?"
@ -583,7 +598,7 @@ interactive_guide() {
#########################
BACKTITLE="Re2o setup - configuration of the mail server"
# Prompt to enter the hostname of the mail server
TITLE="Mail server hostname"
INPUTBOX="The hostname of the mail server to use"
@ -591,7 +606,7 @@ interactive_guide() {
--title "$TITLE" --inputbox "$TITLE" \
$HEIGHT $WIDTH 2>&1 >/dev/tty)"
# Prompt to choose the port of the mail server
# Prompt to choose the port of the mail server
TITLE="Mail server port"
MENU="Which port (thus which protocol) to use to contact the mail server"
OPTIONS=(25 "SMTP"
@ -608,7 +623,7 @@ interactive_guide() {
########################
BACKTITLE="Re2o setup - configuration of the web server"
# Prompt to choose the web server
TITLE="Web server to use"
MENU="Which web server to install for accessing Re2o web frontend (automatic setup of nginx is not supported) ?"
@ -617,14 +632,14 @@ interactive_guide() {
web_serveur="$(dialog --clear --backtitle "$BACKTITLE" \
--title "$TITLE" --menu "$MENU" \
$HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)"
# Prompt to enter the requested URL for the web frontend
TITLE="Web URL"
INPUTBOX="URL for accessing the web server (e.g. re2o.example.net). Be sure that this URL is accessible and correspond to a DNS entry (if applicable)."
url_server="$(dialog --clear --backtitle "$BACKTITLE" \
--title "$TITLE" --inputbox "$INPUTBOX" \
$HEIGHT $WIDTH 2>&1 >/dev/tty)"
# Prompt to choose if the TLS should be setup or not for the web server
TITLE="TLS on web server"
MENU="Would you like to activate the TLS (with Let'Encrypt) on the web server ?"
@ -679,7 +694,7 @@ interactive_guide() {
update_django
create_superuser
install_webserver "$web_serveur" "$is_tls" "$url_server"
@ -748,9 +763,10 @@ main_function() {
echo " * {help} ---------- Display this quick usage documentation"
echo " * {setup} --------- Launch the full interactive guide to setup entirely"
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 " * {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-settings} Interactively rewrite the settings file"
echo " * {reset-db} ------ Erase the previous local database, setup a new empty"
@ -782,9 +798,14 @@ main_function() {
update )
install_requirements
copy_templates_files
update_django
;;
copy-templates-files )
copy_templates_files
;;
update-django )
update_django
;;
@ -800,7 +821,7 @@ main_function() {
reset-db )
if [ ! -z "$2" ]; then
db_password="$2"
case "$3" in
case "$3" in
mysql )
db_engine_type=1;;
postresql )

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\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"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\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_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"
msgstr "Annuler"
@ -113,15 +113,19 @@ msgstr "%(username)s a mis à jour"
#: templates/logs/aff_summary.html:113
#, python-format
msgid "%(username)s has sold %(number)sx %(name)s to"
msgstr "%(username)s a vendu %(number)sx %(name)s à"
msgid "%(username)s has sold %(number)sx %(name)s"
msgstr "%(username)s a vendu %(number)sx %(name)s"
#: templates/logs/aff_summary.html:116
msgid " to"
msgstr " à"
#: templates/logs/aff_summary.html:119
#, python-format
msgid "+%(duration)s months"
msgstr "+%(duration)s mois"
#: templates/logs/aff_summary.html:132
#: templates/logs/aff_summary.html:137
#, python-format
msgid "%(username)s has edited an interface of"
msgstr "%(username)s a modifié une interface de"
@ -149,7 +153,7 @@ msgstr "Confirmer"
msgid "Statistics"
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"
msgstr "Actions effectuées"
@ -173,7 +177,7 @@ msgstr "Base de données"
msgid "Wiring actions"
msgstr "Actions de câblage"
#: templates/logs/sidebar.html:53 views.py:325
#: templates/logs/sidebar.html:53 views.py:336
msgid "Users"
msgstr "Utilisateurs"
@ -189,150 +193,154 @@ msgstr "Statistiques sur la base de données"
msgid "Statistics about users"
msgstr "Statistiques sur les utilisateurs"
#: views.py:191
#: views.py:194
msgid "Nonexistent revision."
msgstr "Révision inexistante."
#: views.py:194
#: views.py:197
msgid "The action was deleted."
msgstr "L'action a été supprimée."
#: views.py:227
#: views.py:230
msgid "Category"
msgstr "Catégorie"
#: views.py:228
#: views.py:231
msgid "Number of users (members and clubs)"
msgstr "Nombre d'utilisateurs (adhérents et clubs)"
#: views.py:229
#: views.py:232
msgid "Number of members"
msgstr "Nombre d'adhérents"
#: views.py:230
#: views.py:233
msgid "Number of clubs"
msgstr "Nombre de clubs"
#: views.py:234
#: views.py:237
msgid "Activated users"
msgstr "Utilisateurs activés"
#: views.py:242
#: views.py:245
msgid "Disabled users"
msgstr "Utilisateurs désactivés"
#: views.py:250
#: views.py:253
msgid "Archived users"
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"
msgstr "Adhérents cotisants"
#: views.py:264
#: views.py:275
msgid "Users benefiting from a connection"
msgstr "Utilisateurs bénéficiant d'une connexion"
#: views.py:270
#: views.py:281
msgid "Banned users"
msgstr "Utilisateurs bannis"
#: views.py:276
#: views.py:287
msgid "Users benefiting from a free connection"
msgstr "Utilisateurs bénéficiant d'une connexion gratuite"
#: views.py:282
#: views.py:293
msgid "Active interfaces (with access to the network)"
msgstr "Interfaces actives (ayant accès au réseau)"
#: views.py:292
#: views.py:303
msgid "Active interfaces assigned IPv4"
msgstr "Interfaces actives assignées IPv4"
#: views.py:305
#: views.py:316
msgid "IP range"
msgstr "Plage d'IP"
#: views.py:306
#: views.py:317
msgid "VLAN"
msgstr "VLAN"
#: views.py:307
#: views.py:318
msgid "Total number of IP addresses"
msgstr "Nombre total d'adresses IP"
#: views.py:308
#: views.py:319
msgid "Number of assigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: views.py:309
#: views.py:320
msgid "Number of IP address assigned to an activated machine"
msgstr "Nombre d'adresses IP assignées à une machine activée"
#: views.py:310
#: views.py:321
msgid "Number of nonassigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: views.py:337
#: views.py:348
msgid "Subscriptions"
msgstr "Cotisations"
#: views.py:359 views.py:420
#: views.py:370 views.py:431
msgid "Machines"
msgstr "Machines"
#: views.py:386
#: views.py:397
msgid "Topology"
msgstr "Topologie"
#: views.py:405
#: views.py:416
msgid "Number of 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"
msgstr "Utilisateur"
#: views.py:423
#: views.py:434
msgid "Invoice"
msgstr "Facture"
#: views.py:426
#: views.py:437
msgid "Ban"
msgstr "Bannissement"
#: views.py:429
#: views.py:440
msgid "Whitelist"
msgstr "Accès gracieux"
#: views.py:432
#: views.py:443
msgid "Rights"
msgstr "Droits"
#: views.py:436
#: views.py:447
msgid "School"
msgstr "Établissement"
#: views.py:441
#: views.py:452
msgid "Payment method"
msgstr "Moyen de paiement"
#: views.py:446
#: views.py:457
msgid "Bank"
msgstr "Banque"
#: views.py:463
#: views.py:474
msgid "Action"
msgstr "Action"
#: views.py:494
#: views.py:505
msgid "No model found."
msgstr "Aucun modèle trouvé."
#: views.py:500
#: views.py:511
msgid "Nonexistent entry."
msgstr "Entrée inexistante."
#: views.py:507
#: views.py:518
msgid "You don't have the right to access this 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 %}
{% if revisions_list.paginator %}
{% include "pagination.html" with list=revisions_list %}
{% include 'pagination.html' with list=revisions_list %}
{% endif %}
{% 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 "Object type" %}</th>
{% 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 %}
<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></th>
</tr>
@ -65,6 +65,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</table>
{% if revisions_list.paginator %}
{% include "pagination.html" with list=revisions_list %}
{% include 'pagination.html' with list=revisions_list %}
{% endif %}

View file

@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% if versions_list.paginator %}
{% include "pagination.html" with list=versions_list %}
{% include 'pagination.html' with list=versions_list %}
{% endif %}
{% load logs_extra %}
@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<thead>
<tr>
{% 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></th>
</tr>
@ -154,6 +154,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</table>
{% if versions_list.paginator %}
{% include "pagination.html" with list=versions_list %}
{% include 'pagination.html' with list=versions_list %}
{% endif %}

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %}
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %}
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
<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 />

View file

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends 'base.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
@ -28,27 +28,27 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block sidebar %}
{% 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"></i>
{% trans "Summary" %}
</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"></i>
{% trans "Events" %}
</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-area-chart"></i>
{% trans "General" %}
</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>
{% trans "Database" %}
</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>
{% trans "Wiring actions" %}
</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>
{% trans "Users" %}
</a>

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %}
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
<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 />

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %}
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
<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 />

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %}
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
<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 />

View file

@ -1,4 +1,4 @@
{% extends "logs/sidebar.html" %}
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
<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 />

File diff suppressed because it is too large Load diff

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

@ -201,7 +201,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
if interfaces_set:
return str(interfaces_set.domain.name)
else:
return "None"
return _("No name")
@cached_property
def complete_name(self):
@ -340,7 +340,7 @@ class IpType(RevMixin, AclMixin, models.Model):
("use_all_iptype", _("Can use all IP types")),
)
verbose_name = _("IP type")
verbose_name_plural = "IP types"
verbose_name_plural = _("IP types")
@cached_property
def ip_range(self):
@ -534,11 +534,11 @@ class Vlan(RevMixin, AclMixin, models.Model):
dhcpv6_snooping = models.BooleanField(default=False)
igmp = models.BooleanField(
default=False,
help_text="Gestion multicast v4"
help_text=_("v4 multicast management")
)
mld = models.BooleanField(
default=False,
help_text="Gestion multicast v6"
help_text=_("v6 multicast management")
)
class Meta:
@ -559,7 +559,7 @@ class Nas(RevMixin, AclMixin, models.Model):
default_mode = '802.1X'
AUTH = (
('802.1X', '802.1X'),
('Mac-address', 'Mac-address'),
('Mac-address', _("MAC-address")),
)
name = models.CharField(max_length=255, unique=True)
@ -666,7 +666,7 @@ class SOA(RevMixin, AclMixin, models.Model):
utilisée dans les migrations de la BDD. """
return cls.objects.get_or_create(
name=_("SOA to edit"),
mail="postmaser@example.com"
mail="postmaster@example.com"
)[0].pk
@ -934,7 +934,7 @@ class SshFp(RevMixin, AclMixin, models.Model):
machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
pub_key_entry = models.TextField(
help_text="SSH public key",
help_text=_("SSH public key"),
max_length=2048
)
algo = models.CharField(
@ -942,7 +942,7 @@ class SshFp(RevMixin, AclMixin, models.Model):
max_length=32
)
comment = models.CharField(
help_text="Comment",
help_text=_("Comment"),
max_length=255,
null=True,
blank=True
@ -1110,23 +1110,6 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
except:
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()
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):
""" Assigne une ip à l'interface """
free_ips = self.type.ip_type.free_ip()
@ -1146,6 +1129,42 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
self.clean()
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):
self.filter_macaddress()
# On verifie la cohérence en forçant l'extension par la méthode
@ -1153,6 +1172,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
if self.type.ip_type != self.ipv4.ip_type:
raise ValidationError(_("The IPv4 address and the machine type"
" don't match."))
self.validate_unique()
super(Interface, self).save(*args, **kwargs)
@staticmethod
@ -1250,19 +1270,6 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
domain = None
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):
""" A list of IPv6 """
@ -1380,7 +1387,10 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
.filter(interface=self.interface, slaac_ip=True)
.exclude(id=self.id)):
raise ValidationError(_("A SLAAC IP address is already registered."))
prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8')
try:
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 (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
IPv6Address(prefix_v6).exploded[:20]):
@ -1609,7 +1619,7 @@ class Role(RevMixin, AclMixin, models.Model):
ROLE = (
('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")),
@ -1869,7 +1879,8 @@ class OuverturePort(RevMixin, AclMixin, models.Model):
)
class Meta:
verbose_name = _("ports openings")
verbose_name = _("ports opening")
verbose_name_plural = _("ports openings")
def __str__(self):
if self.begin == self.end:

View file

@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ alias }}</td>
<td class="text-right">
{% can_edit alias %}
{% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %}
{% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %}
{% acl_end %}
{% history_button alias %}
</td>

View file

@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ dname.dns_entry }}</td>
<td class="text-right">
{% can_edit dname %}
{% include 'buttons/edit.html' with href='machines:edit-dname' id=dname.id %}
{% include 'buttons/edit.html' with href='machines:edit-dname' id=dname.id %}
{% acl_end %}
{% history_button dname %}
</td>

View file

@ -54,7 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ extension.dnssec|tick }}</td>
<td class="text-right">
{% can_edit extension %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
{% acl_end %}
{% history_button extension %}
</td>

View file

@ -56,7 +56,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ type.ouverture_ports }}</td>
<td class="text-right">
{% can_edit type %}
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}
{% acl_end %}
{% history_button type %}
</td>

View file

@ -40,10 +40,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ ipv6.slaac_ip }}</td>
<td class="text-right">
{% can_edit ipv6 %}
{% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %}
{% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %}
{% acl_end %}
{% can_delete ipv6 %}
{% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %}
{% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %}
{% acl_end %}
{% history_button ipv6 %}
</td>

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="table-responsive">
{% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %}
{% include 'pagination.html' with list=machines_list go_to_id="machines" %}
{% endif %}
<table class="table" id="machines_table">
@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</colgroup>
<thead>
{% 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 "MAC address" %}</th>
<th>{% trans "IP address" %}</th>
@ -52,19 +52,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td colspan="4">
{% trans "No name" as tr_no_name %}
{% trans "View the profile" as tr_view_the_profile %}
<b>{{ machine.get_name|default:'<i>tr_no_name</i>' }}</b> <i class="fa 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>
<i class="fa fa-user"></i> {{ machine.user }}
</a>
</td>
<td class="text-right">
{% can_create Interface machine.id %}
{% trans "Create an interface" as tr_create_an_interface %}
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %}
{% can_create Interface machine.id %}
{% trans "Create an interface" as tr_create_an_interface %}
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %}
{% acl_end %}
{% history_button machine %}
{% can_delete machine %}
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
{% acl_end %}
</td>
</tr>
@ -153,7 +153,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
{% history_button interface %}
{% can_delete interface %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
{% acl_end %}
</div>
</td>
@ -215,6 +215,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</script>
{% if machines_list.paginator %}
{% include "pagination.html" with list=machines_list %}
{% include 'pagination.html' with list=machines_list go_to_id="machines" %}
{% endif %}
</div>

View file

@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ type.ip_type }}</td>
<td class="text-right">
{% can_edit type %}
{% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %}
{% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %}
{% acl_end %}
{% history_button type %}
</td>

View file

@ -42,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ mx.name }}</td>
<td class="text-right">
{% can_edit mx %}
{% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %}
{% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %}
{% acl_end %}
{% history_button mx %}
</td>

View file

@ -47,7 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ nas.autocapture_mac|tick }}</td>
<td class="text-right">
{% can_edit nas %}
{% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %}
{% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %}
{% acl_end %}
{% history_button nas %}
</td>

View file

@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ ns.ns }}</td>
<td class="text-right">
{% can_edit ns %}
{% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %}
{% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %}
{% acl_end %}
{% history_button ns %}
</td>

View file

@ -44,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{% for serv in role.servers.all %}{{ serv }}, {% endfor %}</td>
<td class="text-right">
{% can_edit role %}
{% include 'buttons/edit.html' with href='machines:edit-role' id=role.id %}
{% include 'buttons/edit.html' with href='machines:edit-role' id=role.id %}
{% acl_end %}
{% history_button role %}
</td>

View file

@ -47,7 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
class="fa fa-sync"></i></a></td>
<td class="text-right">
{% can_edit service %}
{% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %}
{% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %}
{% acl_end %}
{% history_button service %}
</td>

View file

@ -48,7 +48,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ soa.ttl }}</td>
<td class="text-right">
{% can_edit soa %}
{% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
{% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
{% acl_end %}
{% history_button soa %}
</td>

View file

@ -52,7 +52,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ srv.target }}</td>
<td class="text-right">
{% can_edit srv %}
{% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %}
{% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %}
{% acl_end %}
{% history_button srv %}
</td>

View file

@ -41,11 +41,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ sshfp.comment }}</td>
<td class="text-right">
{% can_edit sshfp %}
{% include 'buttons/edit.html' with href='machines:edit-sshfp' id=sshfp.id %}
{% include 'buttons/edit.html' with href='machines:edit-sshfp' id=sshfp.id %}
{% acl_end %}
{% history_button sshfp %}
{% can_delete sshfp %}
{% include 'buttons/suppr.html' with href='machines:del-sshfp' id=sshfp.id %}
{% include 'buttons/suppr.html' with href='machines:del-sshfp' id=sshfp.id %}
{% acl_end %}
</td>
</tr>

View file

@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ txt.dns_entry }}</td>
<td class="text-right">
{% can_edit txt %}
{% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
{% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
{% acl_end %}
{% history_button txt %}
</td>

View file

@ -45,7 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{% for range in vlan.iptype_set.all %}{{ range }}, {% endfor %}</td>
<td class="text-right">
{% can_edit vlan %}
{% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %}
{% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %}
{% acl_end %}
{% history_button vlan %}
</td>

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -26,14 +26,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %}
{% load i18n %}
{% block title %}{% trans "Creation and editing of machines" %}{% endblock %}
{% block title %}{% trans "Deletion of machines" %}{% endblock %}
{% block content %}
<form class="form" method="post">
{% csrf_token %}
<h4>{% blocktrans %}Warning: are you sure you want to delete this object {{ objet_name }} ( {{ objet }}
)?{% endblocktrans %}</h4>
<h4>{% blocktrans %}Warning: are you sure you want to delete this object {{ objet_name }} ( {{ objet }} )?{% endblocktrans %}</h4>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon='trash' button_class='btn-danger' %}
</form>

View file

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

View file

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

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
class="fa fa-plus"></i>{% trans " Add an alias" %}</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-alias' interface_id %}"><i
class="fa fa-trash"></i>{% trans " Delete one or several aliases" %}</a>
{% include "machines/aff_alias.html" with alias_list=alias_list %}
{% include 'machines/aff_alias.html' with alias_list=alias_list %}
<br/>
<br/>
<br/>

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -39,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-extension' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several extensions" %}
</a>
{% include "machines/aff_extension.html" with extension_list=extension_list %}
{% include 'machines/aff_extension.html' with extension_list=extension_list %}
<h2>{% trans "List of SOA records" %}</h2>
{% can_create SOA %}
@ -50,7 +50,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-soa' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several SOA records" %}
</a>
{% include "machines/aff_soa.html" with soa_list=soa_list %}
{% include 'machines/aff_soa.html' with soa_list=soa_list %}
<h2>{% trans "List of MX records" %}</h2>
{% can_create Mx %}
@ -61,7 +61,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-mx' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several MX records" %}
</a>
{% include "machines/aff_mx.html" with mx_list=mx_list %}
{% include 'machines/aff_mx.html' with mx_list=mx_list %}
<h2>{% trans "List of NS records" %}</h2>
{% can_create Ns %}
@ -72,7 +72,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-ns' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several NS records" %}
</a>
{% include "machines/aff_ns.html" with ns_list=ns_list %}
{% include 'machines/aff_ns.html' with ns_list=ns_list %}
<h2>{% trans "List of TXT records" %}</h2>
{% can_create Txt %}
@ -83,7 +83,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-txt' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several TXT records" %}
</a>
{% include "machines/aff_txt.html" with txt_list=txt_list %}
{% include 'machines/aff_txt.html' with txt_list=txt_list %}
<h2>{% trans "List of DNAME records" %}</h2>
{% can_create DName %}
@ -94,7 +94,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-dname' %}">
<i class="fa fa-trash"></i> {% trans " Delete one or several DNAME records" %}
</a>
{% include "machines/aff_dname.html" with dname_list=dname_list %}
{% include 'machines/aff_dname.html' with dname_list=dname_list %}
<h2>{% trans "List of SRV records" %}</h2>
{% can_create Srv %}
@ -105,5 +105,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-srv' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several SRV records" %}
</a>
{% include "machines/aff_srv.html" with srv_list=srv_list %}
{% include 'machines/aff_srv.html' with srv_list=srv_list %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -39,5 +39,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-iptype' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several IP types" %}
</a>
{% include "machines/aff_iptype.html" with iptype_list=iptype_list %}
{% include 'machines/aff_iptype.html' with iptype_list=iptype_list %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -36,7 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-plus"></i>{% trans " Add an IPv6 address" %}
</a>
{% acl_end %}
{% include "machines/aff_ipv6.html" with ipv6_list=ipv6_list %}
{% include 'machines/aff_ipv6.html' with ipv6_list=ipv6_list %}
<br/>
<br/>
<br/>

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-machinetype' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several machine types" %}
</a>
{% include "machines/aff_machinetype.html" with machinetype_list=machinetype_list %}
{% include 'machines/aff_machinetype.html' with machinetype_list=machinetype_list %}
<br/>
<br/>
<br/>

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-nas' %}">
<i class="fa fa-trash"></i>{% trans " Delete one or several NAS device types" %}
</a>
{% include "machines/aff_nas.html" with nas_list=nas_list %}
{% include 'machines/aff_nas.html' with nas_list=nas_list %}
<br/>
<br/>
<br/>

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/sidebar.html' %}
{% load bootstrap3 %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -37,5 +37,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-role' %}"><i
class="fa fa-trash"></i>{% trans " Delete one or several roles" %}</a>
{% include "machines/aff_role.html" with role_list=role_list %}
{% include 'machines/aff_role.html' with role_list=role_list %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -37,8 +37,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-service' %}"><i
class="fa fa-trash"></i>{% trans " Delete one or several services" %}</a>
{% include "machines/aff_service.html" with service_list=service_list %}
{% include 'machines/aff_service.html' with service_list=service_list %}
<h2>{% trans "States of servers" %}</h2>
{% include "machines/aff_servers.html" with servers_list=servers_list %}
{% include 'machines/aff_servers.html' with servers_list=servers_list %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -34,5 +34,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-plus"></i>{% trans " Add an SSH fingerprint" %}
</a>
{% acl_end %}
{% include "machines/aff_sshfp.html" with sshfp_list=sshfp_list %}
{% include 'machines/aff_sshfp.html' with sshfp_list=sshfp_list %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -37,5 +37,5 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-vlan' %}"><i
class="fa fa-trash"></i>{% trans " Delete one or several VLANs" %}</a>
{% include "machines/aff_vlan.html" with vlan_list=vlan_list %}
{% include 'machines/aff_vlan.html' with vlan_list=vlan_list %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "machines/sidebar.html" %}
{% extends 'machines/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
@ -171,3 +171,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
</form>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends 'base.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
@ -28,57 +28,58 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block sidebar %}
{% can_view_all Machine %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index' %}">
<i class="fa fa-list-ul"></i>
{% trans "Machines" %}
</a>
{% acl_end %}
{% can_view_all MachineType %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-machinetype" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-machinetype' %}">
<i class="fa fa-list-ul"></i>
{% trans "Machine types" %}
</a>
{% acl_end %}
{% can_view_all Extension %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-extension" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-extension' %}">
<i class="fa fa-list-ul"></i>
{% trans "Extensions and zones" %}
</a>
{% acl_end %}
{% can_view_all IpType %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-iptype" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-iptype' %}">
<i class="fa fa-list-ul"></i>
{% trans "IP ranges" %}
</a>
{% acl_end %}
{% can_view_all Vlan %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-vlan" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-vlan' %}">
<i class="fa fa-list-ul"></i>
{% trans "VLANs" %}
</a>
{% acl_end %}
{% can_view_all Nas %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-nas" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-nas' %}">
<i class="fa fa-list-ul"></i>
{% trans "NAS devices" %}
</a>
{% acl_end %}
{% can_view_all machines.Service %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-service" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-service' %}">
<i class="fa fa-list-ul"></i>
{% trans "Services (DHCP, DNS, ...)" %}
</a>
{% acl_end %}
{% can_view_all Role %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-role" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-role' %}">
<i class="fa fa-list-ul"></i>
{% trans "Server roles" %}
</a>
{% acl_end %}
{% can_view_all OuverturePortList %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-portlist" %}">
<a class="list-group-item list-group-item-info" href="{% url 'machines:index-portlist' %}">
<i class="fa fa-list-ul"></i>
{% trans "Ports openings" %}
</a>
{% acl_end %}
{% endblock %}

View file

@ -40,7 +40,8 @@ from .models import (
HomeOption,
RadiusKey,
SwitchManagementCred,
Reminder
Reminder,
DocumentTemplate
)
@ -101,6 +102,12 @@ class ReminderAdmin(VersionAdmin):
"""Class reminder for switch"""
pass
class DocumentTemplateAdmin(VersionAdmin):
"""Admin class for DocumentTemplate"""
pass
admin.site.register(OptionalUser, OptionalUserAdmin)
admin.site.register(OptionalMachine, OptionalMachineAdmin)
admin.site.register(OptionalTopologie, OptionalTopologieAdmin)
@ -113,3 +120,4 @@ admin.site.register(RadiusKey, RadiusKeyAdmin)
admin.site.register(SwitchManagementCred, SwitchManagementCredAdmin)
admin.site.register(AssoOption, AssoOptionAdmin)
admin.site.register(MailMessageOption, MailMessageOptionAdmin)
admin.site.register(DocumentTemplate, DocumentTemplateAdmin)

View file

@ -43,9 +43,12 @@ from .models import (
RadiusKey,
SwitchManagementCred,
RadiusOption,
CotisationsOption,
DocumentTemplate
)
from topologie.models import Switch
class EditOptionalUserForm(ModelForm):
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
class Meta:
@ -115,11 +118,6 @@ class EditOptionalTopologieForm(ModelForm):
prefix=prefix,
**kwargs
)
self.fields['radius_general_policy'].label = _("RADIUS general policy")
self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted"
" by RADIUS")
self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected"
" by RADIUS")
self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
@ -187,9 +185,6 @@ class EditAssoOptionForm(ModelForm):
self.fields['pseudo'].label = _("Usual name")
self.fields['utilisateur_asso'].label = _("Account used for editing"
" from /admin")
self.fields['payment'].label = _("Payment")
self.fields['payment_id'].label = _("Payment ID")
self.fields['payment_pass'].label = _("Payment password")
self.fields['description'].label = _("Description")
@ -236,6 +231,34 @@ class EditRadiusOptionForm(ModelForm):
model = RadiusOption
fields = '__all__'
def clean(self):
cleaned_data = super().clean()
ignored=('radius_general_policy', 'vlan_decision_ok')
fields = (
f for f in self.fields.keys()
if 'vlan' not in f and f not in ignored
)
for f in fields:
choice = cleaned_data.get(f)
vlan = cleaned_data.get(f+'_vlan')
if choice == RadiusOption.SET_VLAN and vlan is None:
self.add_error(
f,
_("You chose to set vlan but did not set any VLAN."),
)
self.add_error(
f+'_vlan',
_("Please, choose a VLAN."),
)
return cleaned_data
class EditCotisationsOptionForm(ModelForm):
"""Edition forms for Cotisations options"""
class Meta:
model = CotisationsOption
fields = '__all__'
class ServiceForm(ModelForm):
"""Edition, ajout de services sur la page d'accueil"""
@ -343,7 +366,7 @@ class DelMailContactForm(Form):
"""Delete contact email adress"""
mailcontacts = forms.ModelMultipleChoiceField(
queryset=MailContact.objects.none(),
label="Enregistrements adresses actuels",
label=_("Current email addresses"),
widget=forms.CheckboxSelectMultiple
)
@ -355,3 +378,36 @@ class DelMailContactForm(Form):
else:
self.fields['mailcontacts'].queryset = MailContact.objects.all()
class DocumentTemplateForm(FormRevMixin, ModelForm):
"""
Form used to create a document template.
"""
class Meta:
model = DocumentTemplate
fields = '__all__'
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(DocumentTemplateForm, self).__init__(
*args, prefix=prefix, **kwargs)
class DelDocumentTemplateForm(FormRevMixin, Form):
"""
Form used to delete one or more document templatess.
The use must choose the one to delete by checking the boxes.
"""
document_templates = forms.ModelMultipleChoiceField(
queryset=DocumentTemplate.objects.none(),
label=_("Available document templates"),
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
instances = kwargs.pop('instances', None)
super(DelDocumentTemplateForm, self).__init__(*args, **kwargs)
if instances:
self.fields['document_templates'].queryset = instances
else:
self.fields['document_templates'].queryset = Banque.objects.all()

File diff suppressed because it is too large Load diff

View file

@ -7,19 +7,6 @@ import django.db.models.deletion
import re2o.mixins
def create_radius_policy(apps, schema_editor):
OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie')
RadiusOption = apps.get_model('preferences', 'RadiusOption')
option,_ = OptionalTopologie.objects.get_or_create()
radius_option = RadiusOption()
radius_option.radius_general_policy = option.radius_general_policy
radius_option.vlan_decision_ok = option.vlan_decision_ok
radius_option.save()
class Migration(migrations.Migration):
dependencies = [
@ -94,18 +81,4 @@ class Migration(migrations.Migration):
name='vlan_decision_ok',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'),
),
migrations.RunPython(create_radius_policy),
migrations.RemoveField(
model_name='optionaltopologie',
name='radius_general_policy',
),
migrations.RemoveField(
model_name='optionaltopologie',
name='vlan_decision_nok',
),
migrations.RemoveField(
model_name='optionaltopologie',
name='vlan_decision_ok',
),
]

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-10-13 14:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
def create_radius_policy(apps, schema_editor):
OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie')
RadiusOption = apps.get_model('preferences', 'RadiusOption')
option,_ = OptionalTopologie.objects.get_or_create()
radius_option = RadiusOption()
radius_option.radius_general_policy = option.radius_general_policy
radius_option.vlan_decision_ok = option.vlan_decision_ok
radius_option.save()
def revert_radius(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('machines', '0095_auto_20180919_2225'),
('preferences', '0055_generaloption_main_site_url'),
('preferences', '0056_1_radiusoption'),
]
operations = [
migrations.RunPython(create_radius_policy, revert_radius),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-10-13 14:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
('machines', '0095_auto_20180919_2225'),
('preferences', '0055_generaloption_main_site_url'),
('preferences', '0056_2_radiusoption'),
]
operations = [
migrations.RemoveField(
model_name='optionaltopologie',
name='radius_general_policy',
),
migrations.RemoveField(
model_name='optionaltopologie',
name='vlan_decision_nok',
),
migrations.RemoveField(
model_name='optionaltopologie',
name='vlan_decision_ok',
),
]

View file

@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0056_radiusoption'),
('preferences', '0056_3_radiusoption'),
]
operations = [

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-05 17:15
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0056_4_radiusoption'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='all_users_active',
field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid'),
),
]

View file

@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-08 22:50
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.aes_field
class Migration(migrations.Migration):
dependencies = [
('preferences', '0057_optionaluser_all_users_active'),
]
operations = [
migrations.AlterModelOptions(
name='radiuskey',
options={'permissions': (('view_radiuskey', 'Can view a RADIUS key object'),), 'verbose_name': 'RADIUS key', 'verbose_name_plural': 'RADIUS keys'},
),
migrations.AlterModelOptions(
name='radiusoption',
options={'verbose_name': 'RADIUS policy', 'verbose_name_plural': 'RADIUS policies'},
),
migrations.AlterModelOptions(
name='reminder',
options={'permissions': (('view_reminder', 'Can view a reminder object'),), 'verbose_name': 'reminder', 'verbose_name_plural': 'reminders'},
),
migrations.AlterModelOptions(
name='switchmanagementcred',
options={'permissions': (('view_switchmanagementcred', 'Can view a switch management credentials object'),), 'verbose_name': 'switch management credentials'},
),
migrations.AlterField(
model_name='mailmessageoption',
name='welcome_mail_en',
field=models.TextField(default='', help_text='Welcome email in English'),
),
migrations.AlterField(
model_name='mailmessageoption',
name='welcome_mail_fr',
field=models.TextField(default='', help_text='Welcome email in French'),
),
migrations.AlterField(
model_name='optionaltopologie',
name='sftp_login',
field=models.CharField(blank=True, help_text='SFTP login for switches', max_length=32, null=True),
),
migrations.AlterField(
model_name='optionaltopologie',
name='sftp_pass',
field=re2o.aes_field.AESEncryptedField(blank=True, help_text='SFTP password', max_length=63, null=True),
),
migrations.AlterField(
model_name='optionaltopologie',
name='switchs_ip_type',
field=models.OneToOneField(blank=True, help_text='IP range for the management of switches', null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.IpType'),
),
migrations.AlterField(
model_name='optionaltopologie',
name='switchs_provision',
field=models.CharField(choices=[('sftp', 'sftp'), ('tftp', 'tftp')], default='tftp', help_text='Provision of configuration mode for switches', max_length=32),
),
migrations.AlterField(
model_name='optionaltopologie',
name='switchs_rest_management',
field=models.BooleanField(default=False, help_text='REST management, activated in case of automatic provision'),
),
migrations.AlterField(
model_name='optionaltopologie',
name='switchs_web_management',
field=models.BooleanField(default=False, help_text='Web management, activated in case of automatic provision'),
),
migrations.AlterField(
model_name='optionaltopologie',
name='switchs_web_management_ssl',
field=models.BooleanField(default=False, help_text='SSL web management, make sure that a certificate is installed on the switch'),
),
migrations.AlterField(
model_name='optionaluser',
name='all_can_create_adherent',
field=models.BooleanField(default=False, help_text='Users can create a member.'),
),
migrations.AlterField(
model_name='optionaluser',
name='all_can_create_club',
field=models.BooleanField(default=False, help_text='Users can create a club.'),
),
migrations.AlterField(
model_name='optionaluser',
name='all_users_active',
field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid.'),
),
migrations.AlterField(
model_name='optionaluser',
name='delete_notyetactive',
field=models.IntegerField(default=15, help_text='Not yet active users will be deleted after this number of days.'),
),
migrations.AlterField(
model_name='optionaluser',
name='local_email_accounts_enabled',
field=models.BooleanField(default=False, help_text='Enable local email accounts for users.'),
),
migrations.AlterField(
model_name='optionaluser',
name='max_email_address',
field=models.IntegerField(default=15, help_text='Maximum number of local email addresses for a standard user.'),
),
migrations.AlterField(
model_name='optionaluser',
name='self_adhesion',
field=models.BooleanField(default=False, help_text='A new user can create their account on Re2o.'),
),
migrations.AlterField(
model_name='optionaluser',
name='self_change_room',
field=models.BooleanField(default=False, help_text='Users can edit their room.'),
),
migrations.AlterField(
model_name='optionaluser',
name='self_change_shell',
field=models.BooleanField(default=False, help_text='Users can edit their shell.'),
),
migrations.AlterField(
model_name='radiuskey',
name='comment',
field=models.CharField(blank=True, help_text='Comment for this key', max_length=255, null=True),
),
migrations.AlterField(
model_name='radiuskey',
name='default_switch',
field=models.BooleanField(default=True, help_text='Default key for switches', unique=True),
),
migrations.AlterField(
model_name='radiuskey',
name='radius_key',
field=re2o.aes_field.AESEncryptedField(help_text='RADIUS key', max_length=255),
),
migrations.AlterField(
model_name='radiusoption',
name='banned',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for banned users'),
),
migrations.AlterField(
model_name='radiusoption',
name='banned_vlan',
field=models.ForeignKey(blank=True, help_text='VLAN for banned users if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banned_vlan', to='machines.Vlan', verbose_name='Banned users VLAN'),
),
migrations.AlterField(
model_name='radiusoption',
name='non_member',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for non members'),
),
migrations.AlterField(
model_name='radiusoption',
name='non_member_vlan',
field=models.ForeignKey(blank=True, help_text='VLAN for non members if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='non_member_vlan', to='machines.Vlan', verbose_name='Non members VLAN'),
),
migrations.AlterField(
model_name='radiusoption',
name='unknown_machine_vlan',
field=models.ForeignKey(blank=True, help_text='VLAN for unknown machines if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_vlan', to='machines.Vlan', verbose_name='Unknown machines VLAN'),
),
migrations.AlterField(
model_name='radiusoption',
name='unknown_port',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown ports'),
),
migrations.AlterField(
model_name='radiusoption',
name='unknown_port_vlan',
field=models.ForeignKey(blank=True, help_text='VLAN for unknown ports if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_vlan', to='machines.Vlan', verbose_name='Unknown ports VLAN'),
),
migrations.AlterField(
model_name='radiusoption',
name='unknown_room',
field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machines connecting from unregistered rooms (relevant on ports with STRICT RADIUS mode)'),
),
migrations.AlterField(
model_name='radiusoption',
name='unknown_room_vlan',
field=models.ForeignKey(blank=True, help_text='VLAN for unknown rooms if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_vlan', to='machines.Vlan', verbose_name='Unknown rooms VLAN'),
),
migrations.AlterField(
model_name='reminder',
name='days',
field=models.IntegerField(default=7, help_text="Delay between the email and the membership's end", unique=True),
),
migrations.AlterField(
model_name='reminder',
name='message',
field=models.CharField(blank=True, default='', help_text='Message displayed specifically for this reminder', max_length=255, null=True),
),
migrations.AlterField(
model_name='switchmanagementcred',
name='default_switch',
field=models.BooleanField(default=True, help_text='Default credentials for switches', unique=True),
),
migrations.AlterField(
model_name='switchmanagementcred',
name='management_id',
field=models.CharField(help_text='Switch login', max_length=63),
),
migrations.AlterField(
model_name='switchmanagementcred',
name='management_pass',
field=re2o.aes_field.AESEncryptedField(help_text='Password', max_length=63),
),
]

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2019-01-20 23:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import preferences.models
import re2o.mixins
def create_defaults(apps, schema_editor):
CotisationsOption = apps.get_model('preferences', 'CotisationsOption')
CotisationsOption.objects.get_or_create()
class Migration(migrations.Migration):
dependencies = [
('preferences', '0058_auto_20190108_1650'),
]
operations = [
migrations.CreateModel(
name='CotisationsOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('send_voucher_mail', models.BooleanField(default=False, verbose_name='Send voucher by email when the invoice is controlled.')),
],
options={
'verbose_name': 'cotisations options',
},
bases=(re2o.mixins.AclMixin, models.Model),
),
migrations.CreateModel(
name='DocumentTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('template', models.FileField(upload_to='templates/', verbose_name='template')),
('name', models.CharField(max_length=125, unique=True, verbose_name='name')),
],
options={
'verbose_name': 'document template',
'verbose_name_plural': 'document templates',
},
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
),
migrations.AddField(
model_name='assooption',
name='pres_name',
field=models.CharField(default='', help_text='Displayed on subscription vouchers', max_length=255, verbose_name='President of the association'),
),
migrations.AddField(
model_name='cotisationsoption',
name='invoice_template',
field=models.OneToOneField(default=preferences.models.default_invoice, on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='preferences.DocumentTemplate', verbose_name='Template for invoices'),
),
migrations.AddField(
model_name='cotisationsoption',
name='voucher_template',
field=models.OneToOneField(default=preferences.models.default_voucher, on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='preferences.DocumentTemplate', verbose_name='Template for subscription voucher'),
),
migrations.RunPython(create_defaults),
]

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