Commit beae8ee8 authored by klafyvel's avatar klafyvel

Merge branch 'dev' into '2.7'

Dev

See merge request !398
parents 201b87f3 4f9e0f8a
Pipeline #1782 passed with stage
in 5 minutes and 28 seconds
......@@ -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.
......@@ -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.")
......@@ -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
# 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é."
......@@ -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
......
......@@ -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),
......
......@@ -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())
......
......@@ -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)
......@@ -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
......@@ -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"