mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-22 19:33:11 +00:00
Merge branch 'translation' into 'master'
Translation of cotisation app See merge request federez/re2o!114
This commit is contained in:
commit
a38e56cde4
26 changed files with 2052 additions and 680 deletions
|
@ -25,6 +25,7 @@
|
|||
|
||||
Here are defined some functions to check acl on the application.
|
||||
"""
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
def can_view(user):
|
||||
"""Check if an user can view the application.
|
||||
|
@ -37,4 +38,4 @@ def can_view(user):
|
|||
viewing is granted and msg is a message (can be None).
|
||||
"""
|
||||
can = user.has_module_perms('cotisations')
|
||||
return can, None if can else "Vous ne pouvez pas voir cette application."
|
||||
return can, None if can else _("You don't have the rights to see this application.")
|
||||
|
|
|
@ -20,19 +20,18 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
"""
|
||||
Forms de l'application cotisation de re2o. Dépendance avec les models,
|
||||
importé par les views.
|
||||
Forms for the 'cotisation' app of re2o. It highly depends on
|
||||
:cotisations:models and is mainly used by :cotisations:views.
|
||||
|
||||
Permet de créer une nouvelle facture pour un user (NewFactureForm),
|
||||
et de l'editer (soit l'user avec EditFactureForm,
|
||||
soit le trésorier avec TrezEdit qui a plus de possibilités que self
|
||||
notamment sur le controle trésorier SelectArticleForm est utilisée
|
||||
lors de la creation d'une facture en
|
||||
parrallèle de NewFacture pour le choix des articles désirés.
|
||||
(la vue correspondante est unique)
|
||||
The following forms are mainly used to create, edit or delete
|
||||
anything related to 'cotisations' :
|
||||
* Payments Methods
|
||||
* Banks
|
||||
* Invoices
|
||||
* Articles
|
||||
|
||||
ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter,
|
||||
éditer ou supprimer une banque/moyen de paiement ou un article
|
||||
See the details for each of these operations in the documentation
|
||||
of each of the method.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
@ -40,6 +39,9 @@ from django import forms
|
|||
from django.db.models import Q
|
||||
from django.forms import ModelForm, Form
|
||||
from django.core.validators import MinValueValidator,MaxValueValidator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _l
|
||||
|
||||
from .models import Article, Paiement, Facture, Banque
|
||||
from preferences.models import OptionalUser
|
||||
from users.models import User
|
||||
|
@ -48,17 +50,21 @@ from re2o.field_permissions import FieldPermissionFormMixin
|
|||
from re2o.mixins import FormRevMixin
|
||||
|
||||
class NewFactureForm(FormRevMixin, ModelForm):
|
||||
"""Creation d'une facture, moyen de paiement, banque et numero
|
||||
de cheque"""
|
||||
"""
|
||||
Form used to create a new invoice by using a payment method, a bank and a
|
||||
cheque number.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
# TODO : remove the use of cheque and banque and paiement
|
||||
# for something more generic or at least in English
|
||||
self.fields['cheque'].required = False
|
||||
self.fields['banque'].required = False
|
||||
self.fields['cheque'].label = 'Numero de chèque'
|
||||
self.fields['banque'].empty_label = "Non renseigné"
|
||||
self.fields['paiement'].empty_label = "Séléctionner\
|
||||
un moyen de paiement"
|
||||
self.fields['cheque'].label = _("Cheque number")
|
||||
self.fields['banque'].empty_label = _("Not specified")
|
||||
self.fields['paiement'].empty_label = \
|
||||
_("Select a payment method")
|
||||
paiement_list = Paiement.objects.filter(type_paiement=1)
|
||||
if paiement_list:
|
||||
self.fields['paiement'].widget\
|
||||
|
@ -70,96 +76,119 @@ class NewFactureForm(FormRevMixin, ModelForm):
|
|||
|
||||
def clean(self):
|
||||
cleaned_data = super(NewFactureForm, self).clean()
|
||||
paiement = cleaned_data.get("paiement")
|
||||
cheque = cleaned_data.get("cheque")
|
||||
banque = cleaned_data.get("banque")
|
||||
paiement = cleaned_data.get('paiement')
|
||||
cheque = cleaned_data.get('cheque')
|
||||
banque = cleaned_data.get('banque')
|
||||
if not paiement:
|
||||
raise forms.ValidationError("Le moyen de paiement est obligatoire")
|
||||
elif paiement.type_paiement == "check" and not (cheque and banque):
|
||||
raise forms.ValidationError("Le numéro de chèque et\
|
||||
la banque sont obligatoires.")
|
||||
raise forms.ValidationError(
|
||||
_("A payment method must be specified.")
|
||||
)
|
||||
elif paiement.type_paiement == 'check' and not (cheque and banque):
|
||||
raise forms.ValidationError(
|
||||
_("A cheque number and a bank must be specified.")
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class CreditSoldeForm(NewFactureForm):
|
||||
"""Permet de faire des opérations sur le solde si il est activé"""
|
||||
"""
|
||||
Form used to make some operations on the user's balance if the option is
|
||||
activated.
|
||||
"""
|
||||
class Meta(NewFactureForm.Meta):
|
||||
model = Facture
|
||||
fields = ['paiement', 'banque', 'cheque']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreditSoldeForm, self).__init__(*args, **kwargs)
|
||||
# TODO : change solde to balance
|
||||
self.fields['paiement'].queryset = Paiement.objects.exclude(
|
||||
moyen='solde'
|
||||
).exclude(moyen="Solde")
|
||||
).exclude(moyen='Solde')
|
||||
|
||||
montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True)
|
||||
|
||||
|
||||
class SelectUserArticleForm(FormRevMixin, Form):
|
||||
"""Selection d'un article lors de la creation d'une facture"""
|
||||
"""
|
||||
Form used to select an article during the creation of an invoice for a member.
|
||||
"""
|
||||
article = forms.ModelChoiceField(
|
||||
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')),
|
||||
label="Article",
|
||||
label=_l("Article"),
|
||||
required=True
|
||||
)
|
||||
quantity = forms.IntegerField(
|
||||
label="Quantité",
|
||||
label=_l("Quantity"),
|
||||
validators=[MinValueValidator(1)],
|
||||
required=True
|
||||
)
|
||||
|
||||
|
||||
class SelectClubArticleForm(Form):
|
||||
"""Selection d'un article lors de la creation d'une facture"""
|
||||
"""
|
||||
Form used to select an article during the creation of an invoice for a club.
|
||||
"""
|
||||
article = forms.ModelChoiceField(
|
||||
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')),
|
||||
label="Article",
|
||||
label=_l("Article"),
|
||||
required=True
|
||||
)
|
||||
quantity = forms.IntegerField(
|
||||
label="Quantité",
|
||||
label=_l("Quantity"),
|
||||
validators=[MinValueValidator(1)],
|
||||
required=True
|
||||
)
|
||||
|
||||
|
||||
# TODO : change Facture to Invoice
|
||||
class NewFactureFormPdf(Form):
|
||||
"""Creation d'un pdf facture par le trésorier"""
|
||||
"""
|
||||
Form used to create a custom PDF invoice.
|
||||
"""
|
||||
article = forms.ModelMultipleChoiceField(
|
||||
queryset=Article.objects.all(),
|
||||
label="Article"
|
||||
label=_l("Article")
|
||||
)
|
||||
number = forms.IntegerField(
|
||||
label="Quantité",
|
||||
label=_l("Quantity"),
|
||||
validators=[MinValueValidator(1)]
|
||||
)
|
||||
paid = forms.BooleanField(label="Payé", required=False)
|
||||
dest = forms.CharField(required=True, max_length=255, label="Destinataire")
|
||||
chambre = forms.CharField(required=False, max_length=10, label="Adresse")
|
||||
paid = forms.BooleanField(label=_l("Paid"), required=False)
|
||||
# TODO : change dest field to recipient
|
||||
dest = forms.CharField(required=True, max_length=255, label=_l("Recipient"))
|
||||
# TODO : change chambre field to address
|
||||
chambre = forms.CharField(required=False, max_length=10, label=_l("Address"))
|
||||
# TODO : change fid field to invoice_id
|
||||
fid = forms.CharField(
|
||||
required=True,
|
||||
max_length=10,
|
||||
label="Numéro de la facture"
|
||||
label=_l("Invoice number")
|
||||
)
|
||||
|
||||
|
||||
# TODO : change Facture to Invoice
|
||||
class EditFactureForm(FieldPermissionFormMixin, NewFactureForm):
|
||||
"""Edition d'une facture : moyen de paiement, banque, user parent"""
|
||||
"""
|
||||
Form used to edit an invoice and its fields : payment method, bank,
|
||||
user associated, ...
|
||||
"""
|
||||
class Meta(NewFactureForm.Meta):
|
||||
# TODO : change Facture to Invoice
|
||||
model = Facture
|
||||
fields = '__all__'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# TODO : change Facture to Invoice
|
||||
super(EditFactureForm, self).__init__(*args, **kwargs)
|
||||
self.fields['user'].label = 'Adherent'
|
||||
self.fields['user'].empty_label = "Séléctionner\
|
||||
l'adhérent propriétaire"
|
||||
self.fields['valid'].label = 'Validité de la facture'
|
||||
self.fields['user'].label = _("Member")
|
||||
self.fields['user'].empty_label = \
|
||||
_("Select the proprietary member")
|
||||
self.fields['valid'].label = _("Validated invoice")
|
||||
|
||||
|
||||
class ArticleForm(FormRevMixin, ModelForm):
|
||||
"""Creation d'un article. Champs : nom, cotisation, durée"""
|
||||
"""
|
||||
Form used to create an article.
|
||||
"""
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = '__all__'
|
||||
|
@ -167,15 +196,17 @@ class ArticleForm(FormRevMixin, ModelForm):
|
|||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
self.fields['name'].label = "Désignation de l'article"
|
||||
self.fields['name'].label = _("Article name")
|
||||
|
||||
|
||||
class DelArticleForm(FormRevMixin, Form):
|
||||
"""Suppression d'un ou plusieurs articles en vente. Choix
|
||||
parmis les modèles"""
|
||||
"""
|
||||
Form used to delete one or more of the currently available articles.
|
||||
The user must choose the one to delete by checking the boxes.
|
||||
"""
|
||||
articles = forms.ModelMultipleChoiceField(
|
||||
queryset=Article.objects.none(),
|
||||
label="Articles actuels",
|
||||
label=_l("Existing articles"),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
|
@ -188,26 +219,39 @@ class DelArticleForm(FormRevMixin, Form):
|
|||
self.fields['articles'].queryset = Article.objects.all()
|
||||
|
||||
|
||||
# TODO : change Paiement to Payment
|
||||
class PaiementForm(FormRevMixin, ModelForm):
|
||||
"""Creation d'un moyen de paiement, champ text moyen et type
|
||||
permettant d'indiquer si il s'agit d'un chèque ou non pour le form"""
|
||||
"""
|
||||
Form used to create a new payment method.
|
||||
The 'cheque' type is used to associate a specific behaviour requiring
|
||||
a cheque number and a bank.
|
||||
"""
|
||||
class Meta:
|
||||
model = Paiement
|
||||
# TODO : change moyen to method and type_paiement to payment_type
|
||||
fields = ['moyen', 'type_paiement']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
self.fields['moyen'].label = 'Moyen de paiement à ajouter'
|
||||
self.fields['type_paiement'].label = 'Type de paiement à ajouter'
|
||||
self.fields['moyen'].label = _("Payment method name")
|
||||
self.fields['type_paiement'].label = _("Payment type")
|
||||
self.fields['type_paiement'].help_text = \
|
||||
_("The payement type is used for specific behaviour.\
|
||||
The \"cheque\" type means a cheque number and a bank name\
|
||||
may be added when using this payment method.")
|
||||
|
||||
|
||||
# TODO : change paiement to payment
|
||||
class DelPaiementForm(FormRevMixin, Form):
|
||||
"""Suppression d'un ou plusieurs moyens de paiements, selection
|
||||
parmis les models"""
|
||||
"""
|
||||
Form used to delete one or more payment methods.
|
||||
The user must choose the one to delete by checking the boxes.
|
||||
"""
|
||||
# TODO : change paiement to payment
|
||||
paiements = forms.ModelMultipleChoiceField(
|
||||
queryset=Paiement.objects.none(),
|
||||
label="Moyens de paiement actuels",
|
||||
label=_l("Existing payment method"),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
|
@ -220,23 +264,32 @@ class DelPaiementForm(FormRevMixin, Form):
|
|||
self.fields['paiements'].queryset = Paiement.objects.all()
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
class BanqueForm(FormRevMixin, ModelForm):
|
||||
"""Creation d'une banque, field name"""
|
||||
"""
|
||||
Form used to create a bank.
|
||||
"""
|
||||
class Meta:
|
||||
# TODO : change banque to bank
|
||||
model = Banque
|
||||
fields = ['name']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||
self.fields['name'].label = 'Banque à ajouter'
|
||||
self.fields['name'].label = _("Bank name")
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
class DelBanqueForm(FormRevMixin, Form):
|
||||
"""Selection d'une ou plusieurs banques, pour suppression"""
|
||||
"""
|
||||
Form used to delete one or more banks.
|
||||
The use must choose the one to delete by checking the boxes.
|
||||
"""
|
||||
# TODO : change banque to bank
|
||||
banques = forms.ModelMultipleChoiceField(
|
||||
queryset=Banque.objects.none(),
|
||||
label="Banques actuelles",
|
||||
label=_l("Existing banks"),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
|
@ -249,43 +302,59 @@ class DelBanqueForm(FormRevMixin, Form):
|
|||
self.fields['banques'].queryset = Banque.objects.all()
|
||||
|
||||
|
||||
# TODO : change facture to Invoice
|
||||
class NewFactureSoldeForm(NewFactureForm):
|
||||
"""Creation d'une facture, moyen de paiement, banque et numero
|
||||
de cheque"""
|
||||
"""
|
||||
Form used to create an invoice
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||
self.fields['cheque'].required = False
|
||||
self.fields['banque'].required = False
|
||||
self.fields['cheque'].label = 'Numero de chèque'
|
||||
self.fields['banque'].empty_label = "Non renseigné"
|
||||
self.fields['paiement'].empty_label = "Séléctionner\
|
||||
une bite de paiement"
|
||||
self.fields['cheque'].label = _('Cheque number')
|
||||
self.fields['banque'].empty_label = _("Not specified")
|
||||
self.fields['paiement'].empty_label = \
|
||||
_("Select a payment method")
|
||||
# TODO : change paiement to payment
|
||||
paiement_list = Paiement.objects.filter(type_paiement=1)
|
||||
if paiement_list:
|
||||
self.fields['paiement'].widget\
|
||||
.attrs['data-cheque'] = paiement_list.first().id
|
||||
|
||||
class Meta:
|
||||
# TODO : change facture to invoice
|
||||
model = Facture
|
||||
# TODO : change paiement to payment and baque to bank
|
||||
fields = ['paiement', 'banque']
|
||||
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(NewFactureSoldeForm, self).clean()
|
||||
# TODO : change paiement to payment
|
||||
paiement = cleaned_data.get("paiement")
|
||||
cheque = cleaned_data.get("cheque")
|
||||
# TODO : change banque to bank
|
||||
banque = cleaned_data.get("banque")
|
||||
# TODO : change paiement to payment
|
||||
if not paiement:
|
||||
raise forms.ValidationError("Le moyen de paiement est obligatoire")
|
||||
raise forms.ValidationError(
|
||||
_("A payment method must be specified.")
|
||||
)
|
||||
# TODO : change paiement and banque to payment and bank
|
||||
elif paiement.type_paiement == "check" and not (cheque and banque):
|
||||
raise forms.ValidationError("Le numéro de chèque et\
|
||||
la banque sont obligatoires.")
|
||||
raise forms.ValidationError(
|
||||
_("A cheque number and a bank must be specified.")
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
# TODO : Better name and docstring
|
||||
class RechargeForm(FormRevMixin, Form):
|
||||
"""
|
||||
Form used to refill a user's balance
|
||||
"""
|
||||
value = forms.FloatField(
|
||||
label='Valeur',
|
||||
label=_l("Amount"),
|
||||
min_value=0.01,
|
||||
validators = []
|
||||
)
|
||||
|
@ -297,7 +366,21 @@ class RechargeForm(FormRevMixin, Form):
|
|||
def clean_value(self):
|
||||
value = self.cleaned_data['value']
|
||||
if value < OptionalUser.get_cached_value('min_online_payment'):
|
||||
raise forms.ValidationError("Montant inférieur au montant minimal de paiement en ligne (%s) €" % OptionalUser.get_cached_value('min_online_payment'))
|
||||
raise forms.ValidationError(
|
||||
_("Requested amount is too small. Minimum amount possible : \
|
||||
%(min_online_amount)s €.") % {
|
||||
min_online_amount: OptionalUser.get_cached_value(
|
||||
'min_online_payment'
|
||||
)
|
||||
}
|
||||
)
|
||||
if value + self.user.solde > OptionalUser.get_cached_value('max_solde'):
|
||||
raise forms.ValidationError("Le solde ne peux excéder %s " % OptionalUser.get_cached_value('max_solde'))
|
||||
raise forms.ValidationError(
|
||||
_("Requested amount is too high. Your balance can't exceed \
|
||||
%(max_online_balance)s €.") % {
|
||||
max_online_balance: OptionalUser.get_cached_value(
|
||||
'max_solde'
|
||||
)
|
||||
}
|
||||
)
|
||||
return value
|
||||
|
|
BIN
cotisations/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
cotisations/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
825
cotisations/locale/fr/LC_MESSAGES/django.po
Normal file
825
cotisations/locale/fr/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,825 @@
|
|||
# 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: 2018-03-31 14:05+0000\n"
|
||||
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
|
||||
"Last-Translator: Maël Kervella <dev@maelkervella.eu>\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"
|
||||
|
||||
#: acl.py:41
|
||||
msgid "You don't have the rights to see this application."
|
||||
msgstr "Vous n'avez pas les droits de voir cette application."
|
||||
|
||||
#: forms.py:63 cotisations/forms.py:298 cotisations/models.py:86
|
||||
msgid "Cheque number"
|
||||
msgstr "Numéro de chèque"
|
||||
|
||||
#: forms.py:64 cotisations/forms.py:299
|
||||
msgid "Not specified"
|
||||
msgstr "Non renseigné"
|
||||
|
||||
#: forms.py:66 cotisations/forms.py:301
|
||||
msgid "Select a payment method"
|
||||
msgstr "Sélectionnez un moyen de paiement"
|
||||
|
||||
#: forms.py:83, cotisations/forms.py:325
|
||||
msgid "A payment method must be specified."
|
||||
msgstr "Un moyen de paiement doit être renseigné."
|
||||
|
||||
#: forms.py:87, cotisations/forms.py:330
|
||||
msgid "A cheque number and a bank must be specified."
|
||||
msgstr "Un numéro de chèqe et une banque doivent être renseignés."
|
||||
|
||||
#: forms.py:114 cotisations/forms.py:129 cotisations/forms.py:144
|
||||
#: models.py:243
|
||||
#: templates/cotisations/aff_article.html:31
|
||||
#: templates/cotisations/new_facture.html:50
|
||||
#: templates/cotisations/new_facture_solde.html:44
|
||||
msgid "Article"
|
||||
msgstr "Article"
|
||||
|
||||
#: forms.py:118 cotisations/forms.py:133 cotisations/forms.py:147
|
||||
#: templates/cotisations/edit_facture.html:46
|
||||
msgid "Quantity"
|
||||
msgstr "Quantité"
|
||||
|
||||
#: forms.py:150
|
||||
msgid "Paid"
|
||||
msgstr "Payé"
|
||||
|
||||
#: forms.py:152
|
||||
msgid "Recipient"
|
||||
msgstr "Destinataire"
|
||||
|
||||
#: forms.py:154
|
||||
msgid "Address"
|
||||
msgstr "Adresse"
|
||||
|
||||
#: forms.py:159
|
||||
msgid "Invoice number"
|
||||
msgstr "Numéro de facture"
|
||||
|
||||
#: forms.py:174 cotisations/models.py:403
|
||||
msgid "Member"
|
||||
msgstr "Adhérent"
|
||||
|
||||
#: forms.py:176
|
||||
msgid "Select the proprietary member"
|
||||
msgstr "Sélectionnez l'adhérent propriétaire"
|
||||
|
||||
#: forms.py:177
|
||||
msgid "Validated invoice"
|
||||
msgstr "Facture validée"
|
||||
|
||||
#: forms.py:190
|
||||
msgid "Article name"
|
||||
msgstr "Nom de l'article"
|
||||
|
||||
#: forms.py:199
|
||||
msgid "Existing articles"
|
||||
msgstr "Articles disponibles"
|
||||
|
||||
#: forms.py:225
|
||||
msgid "Payment method name"
|
||||
msgstr "Nom du moyen de paiement"
|
||||
|
||||
#: forms.py:226 cotisations/models.py:505
|
||||
msgid "Payment type"
|
||||
msgstr "Type de paiement"
|
||||
|
||||
#: forms.py:228
|
||||
msgid ""
|
||||
"The payement type is used for specific behaviour. The \"cheque\" "
|
||||
"type means a cheque number and a bank name may be added when "
|
||||
"using this payment method."
|
||||
msgstr ""
|
||||
"Le type de paiement est utilisé pour des comportements spécifiques. Le type "
|
||||
"\"chèque\" permet de spécifier un numéro de chèque et une banque lors de "
|
||||
" l'utilisation de cette méthode."
|
||||
|
||||
#: forms.py:241
|
||||
msgid "Existing payment method"
|
||||
msgstr "Moyen de paiements disponibles"
|
||||
|
||||
#: forms.py:266
|
||||
msgid "Bank name"
|
||||
msgstr "Nom de la banque"
|
||||
|
||||
#: forms.py:276
|
||||
msgid "Existing banks"
|
||||
msgstr "Banques disponibles"
|
||||
|
||||
#: forms.py:337 cotisations/models.py:238
|
||||
msgid "Amount"
|
||||
msgstr "Montant"
|
||||
|
||||
#: forms.py:350
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Requested amount is too small. Minimum amount possible : "
|
||||
"%(min_online_amount)s €."
|
||||
msgstr ""
|
||||
"Montant demandé est trop faible. Montant minimal possible : "
|
||||
"%(min_online_amount)s €"
|
||||
|
||||
#: forms.py:359
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Requested amount is too high. Your balance can't exceed "
|
||||
"%(max_online_balance)s €."
|
||||
msgstr ""
|
||||
"Montant demandé trop grand. Votre solde ne peut excéder "
|
||||
"%(max_online_balance)s €"
|
||||
|
||||
#: models.py:90
|
||||
#: templates/cotisations/aff_cotisations.html:47
|
||||
#: templates/cotisations/control.html:67
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
#: models.py:95
|
||||
#: templates/cotisations/control.html:71
|
||||
msgid "Validated"
|
||||
msgstr "Validé"
|
||||
|
||||
#: models.py:100
|
||||
#: templates/cotisations/control.html:75
|
||||
msgid "Controlled"
|
||||
msgstr "Controllé"
|
||||
|
||||
#: models.py:107
|
||||
msgid "Can change the \"controlled\" state"
|
||||
msgstr "Peut modifier l'état \"controllé\""
|
||||
|
||||
#: models.py:109
|
||||
msgid "Can create a custom PDF invoice"
|
||||
msgstr "Peut crée une facture PDF personnalisée"
|
||||
|
||||
#: models.py:110
|
||||
msgid "Can see an invoice's details"
|
||||
msgstr "Peut voir les détails d'une facture"
|
||||
|
||||
#: models.py:111
|
||||
msgid "Can edit all the previous invoices"
|
||||
msgstr "Peut modifier toutes les factures existantes"
|
||||
|
||||
#: models.py:113 cotisations/models.py:233
|
||||
msgid "Invoice"
|
||||
msgstr "Facture"
|
||||
|
||||
#: models.py:114
|
||||
#: templates/cotisations/index.html:29 templates/cotisations/sidebar.html:40
|
||||
msgid "Invoices"
|
||||
msgstr "Factures"
|
||||
|
||||
#: models.py:152 cotisations/models.py:186
|
||||
msgid "You don't have the right to edit an invoice."
|
||||
msgstr "Vous n'avez pas le droit de modifier une facture."
|
||||
|
||||
#: models.py:154
|
||||
msgid "You don't have the right to edit this user's invoices."
|
||||
msgstr "Vous n'avez pas le droit de modifier les facture de cette utilisateur."
|
||||
|
||||
#: models.py:157
|
||||
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écedement controllée "
|
||||
"ou invalidée."
|
||||
|
||||
#: models.py:163
|
||||
msgid "You don't have the right to delete an invoice."
|
||||
msgstr "Vous n'avez pas le droit de supprimer une facture."
|
||||
|
||||
#: models.py:165
|
||||
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:167
|
||||
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 controllée "
|
||||
"ou invalidée."
|
||||
|
||||
#: models.py:174
|
||||
msgid "You don't have the right to see someone else's invoices history."
|
||||
msgstr ""
|
||||
"Vous n'avez pas le droit de voir l'historique de la facture de "
|
||||
"quelqu'un d'autre."
|
||||
|
||||
#: models.py:176
|
||||
msgid "The invoice has been invalidated."
|
||||
msgstr "La facture a été invalidée."
|
||||
|
||||
#: models.py:182
|
||||
msgid "You don't have the right to edit the controlled state."
|
||||
msgstr "Vous n'avez pas le droit de modifier l'état \"controllé\"."
|
||||
|
||||
#: models.py:224 cotisations/models.py:409
|
||||
#: models.py:536
|
||||
msgid "Connexion"
|
||||
msgstr "Connexion"
|
||||
|
||||
#: models.py:225 cotisations/models.py:410
|
||||
msgid "Membership"
|
||||
msgstr "Adhésion"
|
||||
|
||||
#: models.py:226 cotisations/models.py:405
|
||||
#: models.py:411 cotisations/models.py:538
|
||||
msgid "Both of them"
|
||||
msgstr "Les deux"
|
||||
|
||||
#: models.py:250
|
||||
#: templates/cotisations/aff_article.html:32
|
||||
msgid "Price"
|
||||
msgstr "Prix"
|
||||
|
||||
#: models.py:255 cotisations/models.py:428
|
||||
msgid "Duration (in whole month)"
|
||||
msgstr "Durée (en mois entiers)"
|
||||
|
||||
#: models.py:263 cotisations/models.py:442
|
||||
#: models.py:552
|
||||
msgid "Type of cotisation"
|
||||
msgstr "Type de cotisation"
|
||||
|
||||
#: models.py:268
|
||||
msgid "Can see a purchase's details"
|
||||
msgstr "Peut voir les détails d'un achat"
|
||||
|
||||
#: models.py:269
|
||||
msgid "Can edit all the previous purchases"
|
||||
msgstr "Peut voir les achats existants"
|
||||
|
||||
#: models.py:271 cotisations/models.py:546
|
||||
msgid "Purchase"
|
||||
msgstr "Achat"
|
||||
|
||||
#: models.py:272
|
||||
msgid "Purchases"
|
||||
msgstr "Achat"
|
||||
|
||||
#: models.py:328
|
||||
msgid "A cotisation should always have a duration."
|
||||
msgstr "Une cotisation devrait toujours avoir une durée."
|
||||
|
||||
#: models.py:335
|
||||
msgid "You don't have the right to edit the purchases."
|
||||
msgstr "Vous n'avez pas le droit de modifier les achats."
|
||||
|
||||
#: models.py:337
|
||||
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:340
|
||||
msgid ""
|
||||
"You don't have the right to edit a purchase already controlled or "
|
||||
"invalidated."
|
||||
msgstr ""
|
||||
"Vous n'avez pas le droit de modifier un achat précédement controllé ou "
|
||||
"invalidé."
|
||||
|
||||
#: models.py:346
|
||||
msgid "You don't have the right to delete a purchase."
|
||||
msgstr "Vous n'avez pas le droit de supprimer un achat."
|
||||
|
||||
#: models.py:348
|
||||
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:350
|
||||
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 controllé ou "
|
||||
"invalidé."
|
||||
|
||||
#: models.py:357
|
||||
msgid "You don't have the right to see someone else's purchase history."
|
||||
msgstr "Vous n'avez pas le droit de voir l'historique d'un achat de quelqu'un d'autre."
|
||||
|
||||
#: models.py:404
|
||||
msgid "Club"
|
||||
msgstr "Club"
|
||||
|
||||
#: models.py:416
|
||||
#: templates/cotisations/aff_cotisations.html:40
|
||||
#: templates/cotisations/control.html:60
|
||||
#: templates/cotisations/edit_facture.html:45
|
||||
msgid "Designation"
|
||||
msgstr "Désignation"
|
||||
|
||||
#: models.py:422
|
||||
msgid "Unitary price"
|
||||
msgstr "Prix unitaire"
|
||||
|
||||
#: models.py:434
|
||||
msgid "Type of users concerned"
|
||||
msgstr "Type d'utilisateurs concernés"
|
||||
|
||||
#: models.py:449
|
||||
msgid "Can see an article's details"
|
||||
msgstr "Peut voir les détails d'un article"
|
||||
|
||||
#: models.py:457
|
||||
msgid "Solde is a reserved article name"
|
||||
msgstr "Solde est un nom d'article réservé"
|
||||
|
||||
#: models.py:461
|
||||
msgid "Duration must be specified for a cotisation"
|
||||
msgstr "La durée doit être spécifiée pour une cotisation"
|
||||
|
||||
#: models.py:474
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: models.py:479
|
||||
msgid "Can see a bank's details"
|
||||
msgstr "Peut voir les détails d'une banque"
|
||||
|
||||
#: models.py:481
|
||||
#: templates/cotisations/aff_banque.html:31
|
||||
msgid "Bank"
|
||||
msgstr "Banque"
|
||||
|
||||
#: models.py:482
|
||||
#: templates/cotisations/index_banque.html:30
|
||||
#: templates/cotisations/sidebar.html:50
|
||||
msgid "Banks"
|
||||
msgstr "Banques"
|
||||
|
||||
#: models.py:493
|
||||
msgid "Standard"
|
||||
msgstr "Standard"
|
||||
|
||||
#: models.py:494
|
||||
msgid "Cheque"
|
||||
msgstr "Chèque"
|
||||
|
||||
#: models.py:500
|
||||
msgid "Method"
|
||||
msgstr "Moyen"
|
||||
|
||||
#: models.py:510
|
||||
msgid "Can see a payement's details"
|
||||
msgstr "Peut voir les détails d'un paiement"
|
||||
|
||||
#: models.py:512
|
||||
#: templates/cotisations/aff_cotisations.html:43
|
||||
#: templates/cotisations/aff_paiement.html:31
|
||||
#: templates/cotisations/control.html:63
|
||||
msgid "Payment method"
|
||||
msgstr "Moyen de paiement"
|
||||
|
||||
#: models.py:513
|
||||
#: templates/cotisations/sidebar.html:55
|
||||
msgid "Payment methods"
|
||||
msgstr "Moyens de paiement"
|
||||
|
||||
#: models.py:526
|
||||
msgid "You cannot have multiple payment method of type cheque"
|
||||
msgstr "Vous ne pouvez avoir plusieurs moyens de paiement de type chèque"
|
||||
|
||||
#: models.py:555
|
||||
msgid "Starting date"
|
||||
msgstr "Date de début"
|
||||
|
||||
#: models.py:558
|
||||
msgid "Ending date"
|
||||
msgstr "Date de fin"
|
||||
|
||||
#: models.py:563
|
||||
msgid "Can see a cotisation's details"
|
||||
msgstr "Peut voir les détails d'une cotisation"
|
||||
|
||||
#: models.py:564
|
||||
msgid "Can edit the previous cotisations"
|
||||
msgstr "Peut voir les cotisations existantes"
|
||||
|
||||
#: models.py:569
|
||||
msgid "You don't have the right to edit a cotisation."
|
||||
msgstr "Vous n'avez pas le droit de modifier une cotisation."
|
||||
|
||||
#: models.py:572
|
||||
msgid ""
|
||||
"You don't have the right to edit a cotisation already controlled or "
|
||||
"invalidated."
|
||||
msgstr ""
|
||||
"Vous n'avez pas le droit de modifier une cotisaiton précédement controllée "
|
||||
"ou invalidée."
|
||||
|
||||
#: models.py:578
|
||||
msgid "You don't have the right to delete a cotisation."
|
||||
msgstr "Vous n'avez pas le droit de supprimer une cotisation."
|
||||
|
||||
#: models.py:580
|
||||
msgid ""
|
||||
"You don't have the right to delete a cotisation already controlled or "
|
||||
"invalidated."
|
||||
msgstr ""
|
||||
"Vous n'avez pas le droit de supprimer une cotisation précédement controllée "
|
||||
"ou invalidée."
|
||||
|
||||
#: models.py:587
|
||||
msgid "You don't have the right to see someone else's cotisation history."
|
||||
msgstr ""
|
||||
"Vous n'avez pas le droit de voir l'historique d'une cotisation de "
|
||||
"quelqu'un d'autre."
|
||||
|
||||
#: payment.py:26
|
||||
#, python-format
|
||||
msgid "The payment of %(amount)s € has been accepted."
|
||||
msgstr "Le paiement de %(amount)s € a été accepté."
|
||||
|
||||
#: payment.py:38
|
||||
msgid "The payment has been refused."
|
||||
msgstr "Le paiment a été refusé."
|
||||
|
||||
#: templates/cotisations/aff_article.html:33
|
||||
msgid "Cotisation type"
|
||||
msgstr "Type de cotisation"
|
||||
|
||||
#: templates/cotisations/aff_article.html:34
|
||||
msgid "Duration (month)"
|
||||
msgstr "Durée (mois)"
|
||||
|
||||
#: templates/cotisations/aff_article.html:35
|
||||
msgid "Concerned users"
|
||||
msgstr "Utilisateurs concernés"
|
||||
|
||||
#: templates/cotisations/aff_article.html:48
|
||||
#: templates/cotisations/aff_banque.html:40
|
||||
#: templates/cotisations/aff_cotisations.html:69
|
||||
#: templates/cotisations/aff_cotisations.html:75
|
||||
#: templates/cotisations/aff_paiement.html:40
|
||||
#: templates/cotisations/control.html:104
|
||||
msgid "Edit"
|
||||
msgstr "Modifier"
|
||||
|
||||
#: templates/cotisations/aff_article.html:52
|
||||
#: templates/cotisations/aff_banque.html:44
|
||||
#: templates/cotisations/aff_cotisations.html:90
|
||||
#: templates/cotisations/aff_paiement.html:44
|
||||
msgid "Historique"
|
||||
msgstr "Historique"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:37
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:41
|
||||
#: templates/cotisations/control.html:61
|
||||
msgid "Total price"
|
||||
msgstr "Prix total"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:51
|
||||
#: templates/cotisations/control.html:53
|
||||
msgid "Invoice id"
|
||||
msgstr "Id facture"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:79
|
||||
msgid "Controlled invoice"
|
||||
msgstr "Facture controllé"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:84
|
||||
msgid "Delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:99
|
||||
msgid "PDF"
|
||||
msgstr "PDF"
|
||||
|
||||
#: templates/cotisations/aff_cotisations.html:102
|
||||
msgid "Invalidated invoice"
|
||||
msgstr "Facture invalidée"
|
||||
|
||||
#: templates/cotisations/control.html:30
|
||||
msgid "Invoice control"
|
||||
msgstr "Contrôle des factures"
|
||||
|
||||
#: templates/cotisations/control.html:33
|
||||
msgid "Invoice control and validation"
|
||||
msgstr "Contrôle et validation des factures"
|
||||
|
||||
#: templates/cotisations/control.html:43
|
||||
msgid "Profil"
|
||||
msgstr "Profil"
|
||||
|
||||
#: templates/cotisations/control.html:45
|
||||
msgid "Last name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: templates/cotisations/control.html:49
|
||||
msgid "First name"
|
||||
msgstr "Prénom"
|
||||
|
||||
#: templates/cotisations/control.html:57
|
||||
msgid "User id"
|
||||
msgstr "Id utilisateur"
|
||||
|
||||
#: templates/cotisations/delete.html:29
|
||||
msgid "Deletion of cotisations"
|
||||
msgstr "Supprimer des cotisations"
|
||||
|
||||
#: templates/cotisations/delete.html:36
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Warning. Are you sure you really want te delete this %(object_name)s "
|
||||
"object ( %(objet)s ) ?\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Attention. Êtes-vous vraiment sûr de vouloir supprimer cet objet "
|
||||
"%(object_name)s ( %(objet)s ) ?\n"
|
||||
" "
|
||||
|
||||
#: templates/cotisations/delete.html:40
|
||||
#: templates/cotisations/edit_facture.html:60
|
||||
#: templates/cotisations/new_facture_solde.html:59
|
||||
#: templates/cotisations/recharge.html:42
|
||||
msgid "Confirm"
|
||||
msgstr "Confirmer"
|
||||
|
||||
#: templates/cotisations/edit_facture.html:31
|
||||
#: templates/cotisations/facture.html:30
|
||||
#: templates/cotisations/new_facture.html:30
|
||||
#: templates/cotisations/new_facture_solde.html:30
|
||||
msgid "Invoices creation and edition"
|
||||
msgstr "Création et modification de factures"
|
||||
|
||||
#: templates/cotisations/edit_facture.html:38
|
||||
msgid "Edit the invoice"
|
||||
msgstr "Edition de factures"
|
||||
|
||||
#: templates/cotisations/edit_facture.html:41
|
||||
#: templates/cotisations/new_facture.html:46
|
||||
#: templates/cotisations/new_facture_solde.html:40
|
||||
msgid "Invoice's articles"
|
||||
msgstr "Articles de la facture"
|
||||
|
||||
#: templates/cotisations/index.html:32
|
||||
msgid "Cotisations"
|
||||
msgstr "Cotisations"
|
||||
|
||||
#: templates/cotisations/index_article.html:30
|
||||
msgid "Articles"
|
||||
msgstr "Articles"
|
||||
|
||||
#: templates/cotisations/index_article.html:33
|
||||
msgid "Article types list"
|
||||
msgstr "Liste des types d'articles"
|
||||
|
||||
#: templates/cotisations/index_article.html:36
|
||||
msgid "Add an article type"
|
||||
msgstr "Ajouter un type d'article"
|
||||
|
||||
#: templates/cotisations/index_article.html:40
|
||||
msgid "Delete article types"
|
||||
msgstr "Supprimer des types d'articles"
|
||||
|
||||
#: templates/cotisations/index_banque.html:33
|
||||
msgid "Banks list"
|
||||
msgstr "Liste des banques"
|
||||
|
||||
#: templates/cotisations/index_banque.html:36
|
||||
msgid "Add a bank"
|
||||
msgstr "Ajouter une banque"
|
||||
|
||||
#: templates/cotisations/index_banque.html:40
|
||||
msgid "Delete banks"
|
||||
msgstr "Supprimer des banques"
|
||||
|
||||
#: templates/cotisations/index_paiement.html:30
|
||||
msgid "Payments"
|
||||
msgstr "Paiement"
|
||||
|
||||
#: templates/cotisations/index_paiement.html:33
|
||||
msgid "Payment types list"
|
||||
msgstr "Liste des types de paiement"
|
||||
|
||||
#: templates/cotisations/index_paiement.html:36
|
||||
msgid "Add a payment type"
|
||||
msgstr "Ajouter un type de paiement"
|
||||
|
||||
#: templates/cotisations/index_paiement.html:40
|
||||
msgid "Delete payment types"
|
||||
msgstr "Supprimer un type de paiement"
|
||||
|
||||
#: templates/cotisations/new_facture.html:37
|
||||
#: templates/cotisations/new_facture_solde.html:37
|
||||
msgid "New invoice"
|
||||
msgstr "Nouvelle facture"
|
||||
|
||||
#: templates/cotisations/new_facture.html:39
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" User's balance : %(user.solde)s €\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Solde de l'utilisateur : %(user.solde)s €\n"
|
||||
" "
|
||||
|
||||
#: templates/cotisations/new_facture.html:59
|
||||
#: templates/cotisations/new_facture_solde.html:53
|
||||
msgid "Add an article"
|
||||
msgstr "Ajouter un article"
|
||||
|
||||
#: templates/cotisations/new_facture.html:61
|
||||
#: templates/cotisations/new_facture_solde.html:55
|
||||
msgid ""
|
||||
"\n"
|
||||
" Total price : <span id=\"total_price\">0,00</span> €\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Prix total : <span id=\"total_price\">0,00</span> €\n"
|
||||
" "
|
||||
|
||||
#: templates/cotisations/new_facture.html:65
|
||||
msgid "Create"
|
||||
msgstr "Créer"
|
||||
|
||||
#: templates/cotisations/payment.html:30 templates/cotisations/recharge.html:30
|
||||
#: templates/cotisations/recharge.html:33
|
||||
msgid "Balance refill"
|
||||
msgstr "Rechargement de solde"
|
||||
|
||||
#: templates/cotisations/payment.html:34
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Refill of %(amount)s €\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Recharger de %(amount)s €\n"
|
||||
" "
|
||||
|
||||
#: templates/cotisations/payment.html:40
|
||||
msgid "Pay"
|
||||
msgstr "Payer"
|
||||
|
||||
#: templates/cotisations/recharge.html:35
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Balance : <span class=\"label label-default\">%(request.user.solde)s "
|
||||
"€</span>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Solde : <span class=\"label label-default\">%(request.user.solde)s "
|
||||
"€</span>\n"
|
||||
" "
|
||||
|
||||
#: templates/cotisations/sidebar.html:32
|
||||
msgid "Create an invoice"
|
||||
msgstr "Créer une facture"
|
||||
|
||||
#: templates/cotisations/sidebar.html:35
|
||||
msgid "Control the invoices"
|
||||
msgstr "Contrôler les factures"
|
||||
|
||||
#: templates/cotisations/sidebar.html:45
|
||||
msgid "Available articles"
|
||||
msgstr "Articles disponibles"
|
||||
|
||||
#: views.py:133
|
||||
msgid "Your balance is too low for this operation."
|
||||
msgstr "Votre solde est trop faible pour cette opération."
|
||||
|
||||
#: views.py:163
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The cotisation of %(member_name)s has been extended to "
|
||||
"%(end_date)s."
|
||||
msgstr "La cotisation de %(member_name)s a été étendu jusqu'à %(end_date)s."
|
||||
|
||||
#: views.py:172
|
||||
msgid "The invoice has been created."
|
||||
msgstr "La facture a été créée."
|
||||
|
||||
#: views.py:180 cotisations/views.py:777
|
||||
msgid "You need to choose at least one article."
|
||||
msgstr "Vous devez choisir au moins un article."
|
||||
|
||||
#: views.py:292
|
||||
msgid "The invoice has been successfully edited."
|
||||
msgstr "La facture a été crée avec succès."
|
||||
|
||||
#: views.py:314
|
||||
msgid "The invoice has been successfully deleted."
|
||||
msgstr "La facture a été supprimée avec succès."
|
||||
|
||||
#: views.py:351
|
||||
msgid "Balance successfully updated."
|
||||
msgstr "Solde mis à jour avec succès."
|
||||
|
||||
#: views.py:376
|
||||
msgid "The article has been successfully created."
|
||||
msgstr "L'article a été créé avec succès."
|
||||
|
||||
#: views.py:400
|
||||
msgid "The article has been successfully edited."
|
||||
msgstr "L'article a été modifié avec succès."
|
||||
|
||||
#: views.py:419
|
||||
msgid "The article(s) have been successfully deleted."
|
||||
msgstr "L'(es) article(s) a(ont) été supprimé(s) avec succès. "
|
||||
|
||||
#: views.py:441
|
||||
msgid "The payment method has been successfully created."
|
||||
msgstr "Le moyen de paiement a été créé avec succès."
|
||||
|
||||
#: views.py:465
|
||||
msgid "The payement method has been successfully edited."
|
||||
msgstr "Le moyen de paiement a été modifié avec succès."
|
||||
|
||||
#: views.py:488
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The payment method %(method_name)s has been successfully "
|
||||
"deleted."
|
||||
msgstr "Le moyen de paiement %(method_name)s a été supprimé avec succès."
|
||||
|
||||
#: views.py:496
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The payment method %(method_name)s can't be deleted "
|
||||
"because there are invoices using it."
|
||||
msgstr ""
|
||||
"Le moyen de paiement %(method_name)s ne peut pas être mis à jour car il y a "
|
||||
"des factures l'utilisant."
|
||||
|
||||
#: views.py:519
|
||||
msgid "The bank has been successfully created."
|
||||
msgstr "La banque a été crée avec succès."
|
||||
|
||||
#: views.py:543
|
||||
msgid "The bank has been successfully edited"
|
||||
msgstr "La banque a été modifée avec succès."
|
||||
|
||||
#: views.py:566
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The bank %(bank_name)s has been successfully deleted."
|
||||
msgstr "La banque %(bank_name)s a été supprimée avec succès."
|
||||
|
||||
#: views.py:574
|
||||
#, python-format
|
||||
msgid ""
|
||||
"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:730
|
||||
msgid "The balance is too low for this operation."
|
||||
msgstr "Le solde est trop faible pour cette opération."
|
||||
|
||||
#: views.py:760
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The cotisation of %(member_name)s has been successfully "
|
||||
"extended to %(end_date)s."
|
||||
msgstr "La cotisation de %(member_name)s a été prolongée jusqu'à %(end_date)s."
|
||||
|
||||
#: views.py:769
|
||||
msgid "The invoice has been successuflly created."
|
||||
msgstr "La facture a été créée avec succès."
|
||||
|
||||
#: views.py:796
|
||||
msgid "Online payment is disabled."
|
||||
msgstr "Le paiement en ligne est désactivé."
|
|
@ -21,26 +21,13 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
"""
|
||||
Definition des models bdd pour les factures et cotisation.
|
||||
Pièce maitresse : l'ensemble du code intelligent se trouve ici,
|
||||
dans les clean et save des models ainsi que de leur methodes supplémentaires.
|
||||
The database models for the 'cotisation' app of re2o.
|
||||
The goal is to keep the main actions here, i.e. the 'clean' and 'save'
|
||||
function are higly reposnsible for the changes, checking the coherence of the
|
||||
data and the good behaviour in general for not breaking the database.
|
||||
|
||||
Facture : reliée à un user, elle a un moyen de paiement, une banque (option),
|
||||
une ou plusieurs ventes
|
||||
|
||||
Article : liste des articles en vente, leur prix, etc
|
||||
|
||||
Vente : ensemble des ventes effectuées, reliées à une facture (foreignkey)
|
||||
|
||||
Banque : liste des banques existantes
|
||||
|
||||
Cotisation : objets de cotisation, contenant un début et une fin. Reliées
|
||||
aux ventes, en onetoone entre une vente et une cotisation.
|
||||
Crées automatiquement au save des ventes.
|
||||
|
||||
Post_save et Post_delete : sychronisation des services et régénération
|
||||
des services d'accès réseau (ex dhcp) lors de la vente d'une cotisation
|
||||
par exemple
|
||||
For further details on each of those models, see the documentation details for
|
||||
each.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
@ -55,55 +42,101 @@ from django.core.validators import MinValueValidator
|
|||
from django.db.models import Max
|
||||
from django.utils import timezone
|
||||
from machines.models import regen
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _l
|
||||
|
||||
from re2o.field_permissions import FieldPermissionModelMixin
|
||||
from re2o.mixins import AclMixin, RevMixin
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
||||
""" Définition du modèle des factures. Une facture regroupe une ou
|
||||
plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement
|
||||
et si il y a lieu un numero pour les chèques. Possède les valeurs
|
||||
valides et controle (trésorerie)"""
|
||||
PRETTY_NAME = "Factures émises"
|
||||
"""
|
||||
The model for an invoice. It reprensents the fact that a user paid for
|
||||
something (it can be multiple article paid at once).
|
||||
|
||||
An invoice is linked to :
|
||||
* one or more purchases (one for each article sold that time)
|
||||
* a user (the one who bought those articles)
|
||||
* a payment method (the one used by the user)
|
||||
* (if applicable) a bank
|
||||
* (if applicable) a cheque number.
|
||||
Every invoice is dated throught the 'date' value.
|
||||
An invoice has a 'controlled' value (default : False) which means that
|
||||
someone with high enough rights has controlled that invoice and taken it
|
||||
into account. It also has a 'valid' value (default : True) which means
|
||||
that someone with high enough rights has decided that this invoice was not
|
||||
valid (thus it's like the user never paid for his articles). It may be
|
||||
necessary in case of non-payment.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
|
||||
# TODO : change paiement to payment
|
||||
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
|
||||
# TODO : change banque to bank
|
||||
banque = models.ForeignKey(
|
||||
'Banque',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True)
|
||||
cheque = models.CharField(max_length=255, blank=True)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
valid = models.BooleanField(default=True)
|
||||
control = models.BooleanField(default=False)
|
||||
null=True
|
||||
)
|
||||
# TODO : maybe change to cheque nummber because not evident
|
||||
cheque = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_l("Cheque number")
|
||||
)
|
||||
date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_l("Date")
|
||||
)
|
||||
# TODO : change name to validity for clarity
|
||||
valid = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_l("Validated")
|
||||
)
|
||||
# TODO : changed name to controlled for clarity
|
||||
control = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_l("Controlled")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = False
|
||||
permissions = (
|
||||
("change_facture_control", "Peut changer l'etat de controle"),
|
||||
("change_facture_pdf", "Peut éditer une facture pdf"),
|
||||
("view_facture", "Peut voir un objet facture"),
|
||||
("change_all_facture", "Superdroit, peut modifier toutes les factures"),
|
||||
# TODO : change facture to invoice
|
||||
('change_facture_control', _l("Can change the \"controlled\" state")),
|
||||
# TODO : seems more likely to be call create_facture_pdf or create_invoice_pdf
|
||||
('change_facture_pdf', _l("Can create a custom PDF invoice")),
|
||||
('view_facture', _l("Can see an invoice's details")),
|
||||
('change_all_facture', _l("Can edit all the previous invoices")),
|
||||
)
|
||||
verbose_name = _l("Invoice")
|
||||
verbose_name_plural = _l("Invoices")
|
||||
|
||||
def linked_objects(self):
|
||||
"""Return linked objects : machine and domain.
|
||||
Usefull in history display"""
|
||||
return self.vente_set.all()
|
||||
|
||||
# TODO : change prix to price
|
||||
def prix(self):
|
||||
"""Renvoie le prix brut sans les quantités. Méthode
|
||||
dépréciée"""
|
||||
prix = Vente.objects.filter(
|
||||
"""
|
||||
Returns: the raw price without the quantities.
|
||||
Deprecated, use :total_price instead.
|
||||
"""
|
||||
price = Vente.objects.filter(
|
||||
facture=self
|
||||
).aggregate(models.Sum('prix'))['prix__sum']
|
||||
return prix
|
||||
return price
|
||||
|
||||
# TODO : change prix to price
|
||||
def prix_total(self):
|
||||
"""Prix total : somme des produits prix_unitaire et quantité des
|
||||
ventes de l'objet"""
|
||||
"""
|
||||
Returns: the total price for an invoice. Sum all the articles' prices
|
||||
and take the quantities into account.
|
||||
"""
|
||||
# TODO : change Vente to somethingelse
|
||||
return Vente.objects.filter(
|
||||
facture=self
|
||||
).aggregate(
|
||||
|
@ -114,7 +147,10 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|||
)['total']
|
||||
|
||||
def name(self):
|
||||
"""String, somme des name des ventes de self"""
|
||||
"""
|
||||
Returns : a string with the name of all the articles in the invoice.
|
||||
Used for reprensenting the invoice with a string.
|
||||
"""
|
||||
name = ' - '.join(Vente.objects.filter(
|
||||
facture=self
|
||||
).values_list('name', flat=True))
|
||||
|
@ -122,44 +158,41 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|||
|
||||
def can_edit(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.change_facture'):
|
||||
return False, u"Vous n'avez pas le droit d'éditer les factures"
|
||||
return False, _("You don't have the right to edit an invoice.")
|
||||
elif not user_request.has_perm('cotisations.change_all_facture') and not self.user.can_edit(user_request, *args, **kwargs)[0]:
|
||||
return False, u"Vous ne pouvez pas éditer les factures de cet user protégé"
|
||||
return False, _("You don't have the right to edit this user's invoices.")
|
||||
elif not user_request.has_perm('cotisations.change_all_facture') and\
|
||||
(self.control or not self.valid):
|
||||
return False, u"Vous n'avez pas le droit d'éditer une facture\
|
||||
controlée ou invalidée par un trésorier"
|
||||
return False, _("You don't have the right to edit an invoice already controlled or invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
def can_delete(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.delete_facture'):
|
||||
return False, u"Vous n'avez pas le droit de supprimer une facture"
|
||||
return False, _("You don't have the right to delete an invoice.")
|
||||
if not self.user.can_edit(user_request, *args, **kwargs)[0]:
|
||||
return False, u"Vous ne pouvez pas éditer les factures de cet user protégé"
|
||||
return False, _("You don't have the right to delete this user's invoices.")
|
||||
if self.control or not self.valid:
|
||||
return False, u"Vous ne pouvez pas supprimer une facture\
|
||||
contrôlée ou invalidée par un trésorier"
|
||||
return False, _("You don't have the right to delete an invoice already controlled or invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
def can_view(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.view_facture') and\
|
||||
self.user != user_request:
|
||||
return False, u"Vous ne pouvez pas afficher l'historique d'une\
|
||||
facture d'un autre user que vous sans droit cableur"
|
||||
return False, _("You don't have the right to see someone else's invoices history.")
|
||||
elif not self.valid:
|
||||
return False, u"La facture est invalidée et ne peut être affichée"
|
||||
return False, _("The invoice has been invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def can_change_control(user_request, *args, **kwargs):
|
||||
return user_request.has_perm('cotisations.change_facture_control'), "Vous ne pouvez pas éditer le controle sans droit trésorier"
|
||||
return user_request.has_perm('cotisations.change_facture_control'), _("You don't have the right to edit the \"controlled\" state.")
|
||||
|
||||
@staticmethod
|
||||
def can_change_pdf(user_request, *args, **kwargs):
|
||||
return user_request.has_perm('cotisations.change_facture_pdf'), "Vous ne pouvez pas éditer une facture sans droit trésorier"
|
||||
return user_request.has_perm('cotisations.change_facture_pdf'), _("You don't have the right to edit an invoice.")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Facture, self).__init__(*args, **kwargs)
|
||||
|
@ -173,7 +206,9 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|||
|
||||
@receiver(post_save, sender=Facture)
|
||||
def facture_post_save(sender, **kwargs):
|
||||
"""Post save d'une facture, synchronise l'user ldap"""
|
||||
"""
|
||||
Synchronise the LDAP user after an invoice has been saved.
|
||||
"""
|
||||
facture = kwargs['instance']
|
||||
user = facture.user
|
||||
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
|
||||
|
@ -181,52 +216,92 @@ def facture_post_save(sender, **kwargs):
|
|||
|
||||
@receiver(post_delete, sender=Facture)
|
||||
def facture_post_delete(sender, **kwargs):
|
||||
"""Après la suppression d'une facture, on synchronise l'user ldap"""
|
||||
"""
|
||||
Synchronise the LDAP user after an invoice has been deleted.
|
||||
"""
|
||||
user = kwargs['instance'].user
|
||||
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
|
||||
|
||||
|
||||
# TODO : change Vente to Purchase
|
||||
class Vente(RevMixin, AclMixin, models.Model):
|
||||
"""Objet vente, contient une quantité, une facture parente, un nom,
|
||||
un prix. Peut-être relié à un objet cotisation, via le boolean
|
||||
iscotisation"""
|
||||
PRETTY_NAME = "Ventes effectuées"
|
||||
"""
|
||||
The model defining a purchase. It consist of one type of article being
|
||||
sold. In particular there may be multiple purchases in a single invoice.
|
||||
|
||||
It's reprensentated by:
|
||||
* an amount (the number of items sold)
|
||||
* an invoice (whose the purchase is part of)
|
||||
* an article
|
||||
* (if applicable) a cotisation (which holds some informations about
|
||||
the effect of the purchase on the time agreed for this user)
|
||||
"""
|
||||
|
||||
# TODO : change this to English
|
||||
COTISATION_TYPE = (
|
||||
('Connexion', 'Connexion'),
|
||||
('Adhesion', 'Adhesion'),
|
||||
('All', 'All'),
|
||||
('Connexion', _l("Connexion")),
|
||||
('Adhesion', _l("Membership")),
|
||||
('All', _l("Both of them")),
|
||||
)
|
||||
|
||||
facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
|
||||
number = models.IntegerField(validators=[MinValueValidator(1)])
|
||||
name = models.CharField(max_length=255)
|
||||
prix = models.DecimalField(max_digits=5, decimal_places=2)
|
||||
# TODO : change facture to invoice
|
||||
facture = models.ForeignKey(
|
||||
'Facture',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_l("Invoice")
|
||||
)
|
||||
# TODO : change number to amount for clarity
|
||||
number = models.IntegerField(
|
||||
validators=[MinValueValidator(1)],
|
||||
verbose_name=_l("Amount")
|
||||
)
|
||||
# TODO : change this field for a ForeinKey to Article
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_l("Article")
|
||||
)
|
||||
# TODO : change prix to price
|
||||
# TODO : this field is not needed if you use Article ForeignKey
|
||||
prix = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
verbose_name=_l("Price"))
|
||||
# TODO : this field is not needed if you use Article ForeignKey
|
||||
duration = models.PositiveIntegerField(
|
||||
help_text="Durée exprimée en mois entiers",
|
||||
blank=True,
|
||||
null=True)
|
||||
null=True,
|
||||
verbose_name=_l("Duration (in whole month)")
|
||||
)
|
||||
# TODO : this field is not needed if you use Article ForeignKey
|
||||
type_cotisation = models.CharField(
|
||||
choices=COTISATION_TYPE,
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=255
|
||||
max_length=255,
|
||||
verbose_name=_l("Type of cotisation")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_vente", "Peut voir un objet vente"),
|
||||
("change_all_vente", "Superdroit, peut modifier toutes les ventes"),
|
||||
('view_vente', _l("Can see a purchase's details")),
|
||||
('change_all_vente', _l("Can edit all the previous purchases")),
|
||||
)
|
||||
verbose_name = _l("Purchase")
|
||||
verbose_name_plural = _l("Purchases")
|
||||
|
||||
|
||||
# TODO : change prix_total to total_price
|
||||
def prix_total(self):
|
||||
"""Renvoie le prix_total de self (nombre*prix)"""
|
||||
"""
|
||||
Returns: the total of price for this amount of items.
|
||||
"""
|
||||
return self.prix*self.number
|
||||
|
||||
def update_cotisation(self):
|
||||
"""Mets à jour l'objet related cotisation de la vente, si
|
||||
il existe : update la date de fin à partir de la durée de
|
||||
la vente"""
|
||||
"""
|
||||
Update the related object 'cotisation' if there is one. Based on the
|
||||
duration of the purchase.
|
||||
"""
|
||||
if hasattr(self, 'cotisation'):
|
||||
cotisation = self.cotisation
|
||||
cotisation.date_end = cotisation.date_start + relativedelta(
|
||||
|
@ -234,9 +309,11 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
return
|
||||
|
||||
def create_cotis(self, date_start=False):
|
||||
"""Update et crée l'objet cotisation associé à une facture, prend
|
||||
en argument l'user, la facture pour la quantitéi, et l'article pour
|
||||
la durée"""
|
||||
"""
|
||||
Update and create a 'cotisation' related object if there is a
|
||||
cotisation_type defined (which means the article sold represents
|
||||
a cotisation)
|
||||
"""
|
||||
if not hasattr(self, 'cotisation') and self.type_cotisation:
|
||||
cotisation = Cotisation(vente=self)
|
||||
cotisation.type_cotisation = self.type_cotisation
|
||||
|
@ -264,41 +341,44 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
return
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# On verifie que si iscotisation, duration est présent
|
||||
"""
|
||||
Save a purchase object and check if all the fields are coherents
|
||||
It also update the associated cotisation in the changes have some
|
||||
effect on the user's cotisation
|
||||
"""
|
||||
# Checking that if a cotisation is specified, there is also a duration
|
||||
if self.type_cotisation and not self.duration:
|
||||
raise ValidationError("Cotisation et durée doivent être présents\
|
||||
ensembles")
|
||||
raise ValidationError(
|
||||
_("A cotisation should always have a duration.")
|
||||
)
|
||||
self.update_cotisation()
|
||||
super(Vente, self).save(*args, **kwargs)
|
||||
|
||||
def can_edit(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.change_vente'):
|
||||
return False, u"Vous n'avez pas le droit d'éditer les ventes"
|
||||
return False, _("You don't have the right to edit the purchases.")
|
||||
elif not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
|
||||
return False, u"Vous ne pouvez pas éditer les factures de cet user protégé"
|
||||
return False, _("You don't have the right to edit this user's purchases.")
|
||||
elif not user_request.has_perm('cotisations.change_all_vente') and\
|
||||
(self.facture.control or not self.facture.valid):
|
||||
return False, u"Vous n'avez pas le droit d'éditer une vente\
|
||||
controlée ou invalidée par un trésorier"
|
||||
return False, _("You don't have the right to edit a purchase already controlled or invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
def can_delete(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.delete_vente'):
|
||||
return False, u"Vous n'avez pas le droit de supprimer une vente"
|
||||
return False, _("You don't have the right to delete a purchase.")
|
||||
if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
|
||||
return False, u"Vous ne pouvez pas éditer les factures de cet user protégé"
|
||||
return False, _("You don't have the right to delete this user's purchases.")
|
||||
if self.facture.control or not self.facture.valid:
|
||||
return False, u"Vous ne pouvez pas supprimer une vente\
|
||||
contrôlée ou invalidée par un trésorier"
|
||||
return False, _("You don't have the right to delete a purchase already controlled or invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
def can_view(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.view_vente') and\
|
||||
self.facture.user != user_request:
|
||||
return False, u"Vous ne pouvez pas afficher l'historique d'une\
|
||||
facture d'un autre user que vous sans droit cableur"
|
||||
return False, _("You don't have the right to see someone else's purchase history.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
|
@ -306,81 +386,109 @@ class Vente(RevMixin, AclMixin, models.Model):
|
|||
return str(self.name) + ' ' + str(self.facture)
|
||||
|
||||
|
||||
# TODO : change vente to purchase
|
||||
@receiver(post_save, sender=Vente)
|
||||
def vente_post_save(sender, **kwargs):
|
||||
"""Post save d'une vente, déclencge la création de l'objet cotisation
|
||||
si il y a lieu(si iscotisation) """
|
||||
vente = kwargs['instance']
|
||||
"""
|
||||
Creates a 'cotisation' related object if needed and synchronise the
|
||||
LDAP user when a purchase has been saved.
|
||||
"""
|
||||
purchase = kwargs['instance']
|
||||
if hasattr(vente, 'cotisation'):
|
||||
vente.cotisation.vente = vente
|
||||
vente.cotisation.save()
|
||||
if vente.type_cotisation:
|
||||
vente.create_cotis()
|
||||
vente.cotisation.save()
|
||||
user = vente.facture.user
|
||||
purchase.cotisation.vente = purchase
|
||||
purchase.cotisation.save()
|
||||
if purchase.type_cotisation:
|
||||
purchase.create_cotis()
|
||||
purchase.cotisation.save()
|
||||
user = purchase.facture.user
|
||||
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
|
||||
|
||||
|
||||
# TODO : change vente to purchase
|
||||
@receiver(post_delete, sender=Vente)
|
||||
def vente_post_delete(sender, **kwargs):
|
||||
"""Après suppression d'une vente, on synchronise l'user ldap (ex
|
||||
suppression d'une cotisation"""
|
||||
vente = kwargs['instance']
|
||||
if vente.type_cotisation:
|
||||
user = vente.facture.user
|
||||
"""
|
||||
Synchronise the LDAP user after a purchase has been deleted.
|
||||
"""
|
||||
purchase = kwargs['instance']
|
||||
if purchase.type_cotisation:
|
||||
user = purchase.facture.user
|
||||
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
|
||||
|
||||
|
||||
class Article(RevMixin, AclMixin, models.Model):
|
||||
"""Liste des articles en vente : prix, nom, et attribut iscotisation
|
||||
et duree si c'est une cotisation"""
|
||||
PRETTY_NAME = "Articles en vente"
|
||||
"""
|
||||
The definition of an article model. It represents an type of object that can be sold to the user.
|
||||
|
||||
It's represented by:
|
||||
* a name
|
||||
* a price
|
||||
* a cotisation type (indicating if this article reprensents a cotisation or not)
|
||||
* a duration (if it is a cotisation)
|
||||
* a type of user (indicating what kind of user can buy this article)
|
||||
"""
|
||||
|
||||
# TODO : Either use TYPE or TYPES in both choices but not both
|
||||
USER_TYPES = (
|
||||
('Adherent', 'Adherent'),
|
||||
('Club', 'Club'),
|
||||
('All', 'All'),
|
||||
('Adherent', _l("Member")),
|
||||
('Club', _l("Club")),
|
||||
('All', _l("Both of them")),
|
||||
)
|
||||
|
||||
COTISATION_TYPE = (
|
||||
('Connexion', 'Connexion'),
|
||||
('Adhesion', 'Adhesion'),
|
||||
('All', 'All'),
|
||||
('Connexion', _l("Connexion")),
|
||||
('Adhesion', _l("Membership")),
|
||||
('All', _l("Both of them")),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
prix = models.DecimalField(max_digits=5, decimal_places=2)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_l("Designation")
|
||||
)
|
||||
# TODO : change prix to price
|
||||
prix = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
verbose_name=_l("Unitary price")
|
||||
)
|
||||
duration = models.PositiveIntegerField(
|
||||
help_text="Durée exprimée en mois entiers",
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0)])
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_l("Duration (in whole month)")
|
||||
)
|
||||
type_user = models.CharField(
|
||||
choices=USER_TYPES,
|
||||
default='All',
|
||||
max_length=255
|
||||
max_length=255,
|
||||
verbose_name=_l("Type of users concerned")
|
||||
)
|
||||
type_cotisation = models.CharField(
|
||||
choices=COTISATION_TYPE,
|
||||
default=None,
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=255
|
||||
max_length=255,
|
||||
verbose_name=_l("Type of cotisation")
|
||||
)
|
||||
|
||||
unique_together = ('name', 'type_user')
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_article", "Peut voir un objet article"),
|
||||
('view_article', _l("Can see an article's details")),
|
||||
)
|
||||
verbose_name = "Article"
|
||||
verbose_name_plural = "Articles"
|
||||
|
||||
def clean(self):
|
||||
if self.name.lower() == "solde":
|
||||
raise ValidationError("Solde est un nom d'article invalide")
|
||||
if self.name.lower() == 'solde':
|
||||
raise ValidationError(
|
||||
_("Solde is a reserved article name")
|
||||
)
|
||||
if self.type_cotisation and not self.duration:
|
||||
raise ValidationError(
|
||||
"La durée est obligatoire si il s'agit d'une cotisation"
|
||||
_("Duration must be specified for a cotisation")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -388,99 +496,150 @@ class Article(RevMixin, AclMixin, models.Model):
|
|||
|
||||
|
||||
class Banque(RevMixin, AclMixin, models.Model):
|
||||
"""Liste des banques"""
|
||||
PRETTY_NAME = "Banques enregistrées"
|
||||
"""
|
||||
The model defining a bank. It represents a user's bank. It's mainly used
|
||||
for statistics by regrouping the user under their bank's name and avoid
|
||||
the use of a simple name which leads (by experience) to duplicates that
|
||||
only differs by a capital letter, a space, a misspelling, ... That's why
|
||||
it's easier to use simple object for the banks.
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_l("Name")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_banque", "Peut voir un objet banque"),
|
||||
('view_banque', _l("Can see a bank's details")),
|
||||
)
|
||||
verbose_name=_l("Bank")
|
||||
verbose_name_plural=_l("Banks")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# TODO : change Paiement to Payment
|
||||
class Paiement(RevMixin, AclMixin, models.Model):
|
||||
"""Moyens de paiement"""
|
||||
PRETTY_NAME = "Moyens de paiement"
|
||||
"""
|
||||
The model defining a payment method. It is how the user is paying for the
|
||||
invoice. It's easier to know this information when doing the accouts.
|
||||
It is represented by:
|
||||
* a name
|
||||
* a type (used for the type 'cheque' which implies the use of a bank
|
||||
and an account number in related models)
|
||||
"""
|
||||
|
||||
PAYMENT_TYPES = (
|
||||
(0, 'Autre'),
|
||||
(1, 'Chèque'),
|
||||
(0, _l("Standard")),
|
||||
(1, _l("Cheque")),
|
||||
)
|
||||
|
||||
moyen = models.CharField(max_length=255)
|
||||
type_paiement = models.IntegerField(choices=PAYMENT_TYPES, default=0)
|
||||
# TODO : change moyen to method
|
||||
moyen = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_l("Method")
|
||||
)
|
||||
type_paiement = models.IntegerField(
|
||||
choices=PAYMENT_TYPES,
|
||||
default=0,
|
||||
verbose_name=_l("Payment type")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_paiement", "Peut voir un objet paiement"),
|
||||
('view_paiement', _l("Can see a payement's details")),
|
||||
)
|
||||
verbose_name = _l("Payment method")
|
||||
verbose_name_plural = _l("Payment methods")
|
||||
|
||||
def __str__(self):
|
||||
return self.moyen
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Override of the herited clean function to get a correct name
|
||||
"""
|
||||
self.moyen = self.moyen.title()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Un seul type de paiement peut-etre cheque..."""
|
||||
"""
|
||||
Override of the herited save function to be sure only one payment
|
||||
method of type 'cheque' exists.
|
||||
"""
|
||||
if Paiement.objects.filter(type_paiement=1).count() > 1:
|
||||
raise ValidationError("On ne peut avoir plusieurs mode de paiement\
|
||||
chèque")
|
||||
raise ValidationError(
|
||||
_("You cannot have multiple payment method of type cheque")
|
||||
)
|
||||
super(Paiement, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Cotisation(RevMixin, AclMixin, models.Model):
|
||||
"""Objet cotisation, debut et fin, relié en onetoone à une vente"""
|
||||
PRETTY_NAME = "Cotisations"
|
||||
"""
|
||||
The model defining a cotisation. It holds information about the time a user
|
||||
is allowed when he has paid something.
|
||||
It characterised by :
|
||||
* a date_start (the date when the cotisaiton begins/began
|
||||
* a date_end (the date when the cotisation ends/ended
|
||||
* a type of cotisation (which indicates the implication of such
|
||||
cotisation)
|
||||
* a purchase (the related objects this cotisation is linked to)
|
||||
"""
|
||||
|
||||
COTISATION_TYPE = (
|
||||
('Connexion', 'Connexion'),
|
||||
('Adhesion', 'Adhesion'),
|
||||
('All', 'All'),
|
||||
('Connexion', _l("Connexion")),
|
||||
('Adhesion', _l("Membership")),
|
||||
('All', _l("Both of them")),
|
||||
)
|
||||
|
||||
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
|
||||
# TODO : change vente to purchase
|
||||
vente = models.OneToOneField(
|
||||
'Vente',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
verbose_name=_l("Purchase")
|
||||
)
|
||||
type_cotisation = models.CharField(
|
||||
choices=COTISATION_TYPE,
|
||||
max_length=255,
|
||||
default='All',
|
||||
verbose_name=_l("Type of cotisation")
|
||||
)
|
||||
date_start = models.DateTimeField(
|
||||
verbose_name=_l("Starting date")
|
||||
)
|
||||
date_end = models.DateTimeField(
|
||||
verbose_name=_l("Ending date")
|
||||
)
|
||||
date_start = models.DateTimeField()
|
||||
date_end = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_cotisation", "Peut voir un objet cotisation"),
|
||||
("change_all_cotisation", "Superdroit, peut modifier toutes les cotisations"),
|
||||
('view_cotisation', _l("Can see a cotisation's details")),
|
||||
('change_all_cotisation', _l("Can edit the previous cotisations")),
|
||||
)
|
||||
|
||||
def can_edit(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.change_cotisation'):
|
||||
return False, u"Vous n'avez pas le droit d'éditer les cotisations"
|
||||
return False, _("You don't have the right to edit a cotisation.")
|
||||
elif not user_request.has_perm('cotisations.change_all_cotisation') and\
|
||||
(self.vente.facture.control or not self.vente.facture.valid):
|
||||
return False, u"Vous n'avez pas le droit d'éditer une cotisation\
|
||||
controlée ou invalidée par un trésorier"
|
||||
return False, _("You don't have the right to edit a cotisation already controlled or invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
def can_delete(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.delete_cotisation'):
|
||||
return False, u"Vous n'avez pas le droit de supprimer une cotisations"
|
||||
return False, _("You don't have the right to delete a cotisation.")
|
||||
if self.vente.facture.control or not self.vente.facture.valid:
|
||||
return False, u"Vous ne pouvez pas supprimer une cotisations\
|
||||
contrôlée ou invalidée par un trésorier"
|
||||
return False, _("You don't have the right to delete a cotisation already controlled or invalidated.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
def can_view(self, user_request, *args, **kwargs):
|
||||
if not user_request.has_perm('cotisations.view_cotisation') and\
|
||||
self.vente.facture.user != user_request:
|
||||
return False, u"Vous ne pouvez pas afficher l'historique d'une\
|
||||
cotisation d'un autre user que vous sans droit cableur"
|
||||
return False, _("You don't have the right to see someone else's cotisation history.")
|
||||
else:
|
||||
return True, None
|
||||
|
||||
|
@ -490,16 +649,23 @@ class Cotisation(RevMixin, AclMixin, models.Model):
|
|||
|
||||
@receiver(post_save, sender=Cotisation)
|
||||
def cotisation_post_save(sender, **kwargs):
|
||||
"""Après modification d'une cotisation, regeneration des services"""
|
||||
"""
|
||||
Mark some services as needing a regeneration after the edition of a
|
||||
cotisation. Indeed the membership status may have changed.
|
||||
"""
|
||||
regen('dns')
|
||||
regen('dhcp')
|
||||
regen('mac_ip_list')
|
||||
regen('mailing')
|
||||
|
||||
|
||||
# TODO : should be name cotisation_post_delete
|
||||
@receiver(post_delete, sender=Cotisation)
|
||||
def vente_post_delete(sender, **kwargs):
|
||||
"""Après suppression d'une vente, régénération des services"""
|
||||
"""
|
||||
Mark some services as needing a regeneration after the deletion of a
|
||||
cotisation. Indeed the membership status may have changed.
|
||||
"""
|
||||
cotisation = kwargs['instance']
|
||||
regen('mac_ip_list')
|
||||
regen('mailing')
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.contrib import messages
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.datastructures import MultiValueDictKeyError
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
|
||||
from collections import OrderedDict
|
||||
|
@ -19,10 +20,15 @@ from .payment_utils.comnpay import Payment as ComnpayPayment
|
|||
@csrf_exempt
|
||||
@login_required
|
||||
def accept_payment(request, factureid):
|
||||
"""
|
||||
The view called when an online payment has been accepted.
|
||||
"""
|
||||
facture = get_object_or_404(Facture, id=factureid)
|
||||
messages.success(
|
||||
request,
|
||||
"Le paiement de {} € a été accepté.".format(facture.prix())
|
||||
_("The payment of %(amount)s € has been accepted.") % {
|
||||
amount: facture.prix()
|
||||
}
|
||||
)
|
||||
return redirect(reverse('users:profil', kwargs={'userid':request.user.id}))
|
||||
|
||||
|
@ -30,14 +36,22 @@ def accept_payment(request, factureid):
|
|||
@csrf_exempt
|
||||
@login_required
|
||||
def refuse_payment(request):
|
||||
"""
|
||||
The view called when an online payment has been refused.
|
||||
"""
|
||||
messages.error(
|
||||
request,
|
||||
"Le paiement a été refusé."
|
||||
_("The payment has been refused.")
|
||||
)
|
||||
return redirect(reverse('users:profil', kwargs={'userid':request.user.id}))
|
||||
|
||||
@csrf_exempt
|
||||
def ipn(request):
|
||||
"""
|
||||
The view called by Comnpay server to validate the transaction.
|
||||
Verify that we can firmly save the user's action and notify
|
||||
Comnpay with 400 response if not or with a 200 response if yes
|
||||
"""
|
||||
p = ComnpayPayment()
|
||||
order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
|
||||
try:
|
||||
|
@ -52,7 +66,7 @@ def ipn(request):
|
|||
idTpe = request.POST['idTpe']
|
||||
idTransaction = request.POST['idTransaction']
|
||||
|
||||
# On vérifie que le paiement nous est destiné
|
||||
# Checking that the payment is actually for us.
|
||||
if not idTpe == AssoOption.get_cached_value('payment_id'):
|
||||
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
|
||||
|
||||
|
@ -63,22 +77,28 @@ def ipn(request):
|
|||
|
||||
facture = get_object_or_404(Facture, id=factureid)
|
||||
|
||||
# On vérifie que le paiement est valide
|
||||
# Checking that the payment is valid
|
||||
if not result:
|
||||
# Le paiement a échoué : on effectue les actions nécessaires (On indique qu'elle a échoué)
|
||||
# Payment failed: Cancelling the invoice operation
|
||||
facture.delete()
|
||||
|
||||
# On notifie au serveur ComNPay qu'on a reçu les données pour traitement
|
||||
# And send the response to Comnpay indicating we have well
|
||||
# received the failure information.
|
||||
return HttpResponse("HTTP/1.1 200 OK")
|
||||
|
||||
facture.valid = True
|
||||
facture.save()
|
||||
|
||||
# A nouveau, on notifie au serveur qu'on a bien traité les données
|
||||
# Everything worked we send a reponse to Comnpay indicating that
|
||||
# it's ok for them to proceed
|
||||
return HttpResponse("HTTP/1.0 200 OK")
|
||||
|
||||
|
||||
def comnpay(facture, request):
|
||||
"""
|
||||
Build a request to start the negociation with Comnpay by using
|
||||
a facture id, the price and the secret transaction data stored in
|
||||
the preferences.
|
||||
"""
|
||||
host = request.get_host()
|
||||
p = ComnpayPayment(
|
||||
str(AssoOption.get_cached_value('payment_id')),
|
||||
|
@ -105,6 +125,7 @@ def comnpay(facture, request):
|
|||
return r
|
||||
|
||||
|
||||
# The payment systems supported by re2o
|
||||
PAYMENT_SYSTEM = {
|
||||
'COMNPAY' : comnpay,
|
||||
'NONE' : None
|
||||
|
|
|
@ -23,15 +23,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Article</th>
|
||||
<th>Prix</th>
|
||||
<th>Type Cotisation</th>
|
||||
<th>Durée (mois)</th>
|
||||
<th>Article pour</th>
|
||||
<th>{% trans "Article" %}</th>
|
||||
<th>{% trans "Price" %}</th>
|
||||
<th>{% trans "Cotisation type" %}</th>
|
||||
<th>{% trans "Duration (month)" %}</th>
|
||||
<th>{% trans "Concerned users" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -44,11 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ article.type_user }}</td>
|
||||
<td class="text-right">
|
||||
{% can_edit article %}
|
||||
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'cotisations:edit-article' article.id %}">
|
||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-article' article.id %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% acl_end %}
|
||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'cotisations:history' 'article' article.id %}">
|
||||
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'article' article.id %}">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -23,11 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Banque</th>
|
||||
<th>{% trans "Bank" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -36,11 +37,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ banque.name }}</td>
|
||||
<td class="text-right">
|
||||
{% can_edit banque %}
|
||||
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'cotisations:edit-banque' banque.id %}">
|
||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-banque' banque.id %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% acl_end %}
|
||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'cotisations:history' 'banque' banque.id %}">
|
||||
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'banque' banque.id %}">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -23,20 +23,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% include "buttons/sort.html" with prefix='cotis' col='user' text='Utilisateur' %}</th>
|
||||
<th>Designation</th>
|
||||
<th>Prix total</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='cotis' col='paiement' text='Moyen de paiement' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='cotis' col='date' text='Date' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='cotis' col='id' text='Id facture' %}</th>
|
||||
<th>
|
||||
{% trans "User" as tr_user %}
|
||||
{% include 'buttons/sort.html' with prefix='cotis' col='user' text=tr_user %}
|
||||
</th>
|
||||
<th>{% trans "Designation" %}</th>
|
||||
<th>{% trans "Total price" %}</th>
|
||||
<th>
|
||||
{% trans "Payment method" as tr_payment_method %}
|
||||
{% include 'buttons/sort.html' with prefix='cotis' col='paiement' text=tr_payment_method %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Date" as tr_date %}
|
||||
{% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Invoice id" as tr_invoice_id %}
|
||||
{% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %}
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
@ -51,31 +65,41 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ facture.id }}</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="editionfacture" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
Edition
|
||||
<span class="caret"></span>
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="editinvoice" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
{% trans "Edit" %}<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="editionfacture">
|
||||
<ul class="dropdown-menu" aria-labelledby="editinvoice">
|
||||
{% can_edit facture %}
|
||||
<li><a href="{% url 'cotisations:edit-facture' facture.id %}"><i class="fa fa-dollar-sign"></i> Modifier</a></li>
|
||||
<li>
|
||||
<a href="{% url 'cotisations:edit-facture' facture.id %}">
|
||||
<i class="fa fa-dollar-sign"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
</li>
|
||||
{% acl_else %}
|
||||
<li>Facture controlée</li>
|
||||
<li>{% trans "Controlled invoice" %}</li>
|
||||
{% acl_end %}
|
||||
{% can_delete facture %}
|
||||
<li><a href="{% url 'cotisations:del-facture' facture.id %}"><i class="fa fa-trash"></i> Supprimer</a></li>
|
||||
<li>
|
||||
<a href="{% url 'cotisations:del-facture' facture.id %}">
|
||||
<i class="fa fa-trash"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
</li>
|
||||
{% acl_end %}
|
||||
<li><a href="{% url 'cotisations:history' 'facture' facture.id %}"><i class="fa fa-history"></i> Historique</a></li>
|
||||
<li>
|
||||
<a href="{% url 'cotisations:history' 'facture' facture.id %}">
|
||||
<i class="fa fa-history"></i> {% trans "Historique" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if facture.valid %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:facture-pdf' facture.id %}">
|
||||
<i class="fa fa-file-pdf"></i>
|
||||
PDF
|
||||
<i class="fa fa-file-pdf"></i> {% trans "PDF" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<i class="text-danger">Facture invalide</i>
|
||||
<i class="text-danger">{% trans "Invalidated invoice" %}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -83,6 +107,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</table>
|
||||
|
||||
{% if facture_list.paginator %}
|
||||
{% include "pagination.html" with list=facture_list %}
|
||||
{% include 'pagination.html' with list=facture_list %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -23,11 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Moyen de paiement</th>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -36,11 +37,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td>{{ paiement.moyen }}</td>
|
||||
<td class="text-right">
|
||||
{% can_edit paiement %}
|
||||
<a class="btn btn-primary btn-sm" role="button" title="Éditer" href="{% url 'cotisations:edit-paiement' paiement.id %}">
|
||||
<a class="btn btn-primary btn-sm" role="button" title="{% trans "Edit" %}" href="{% url 'cotisations:edit-paiement' paiement.id %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% acl_end %}
|
||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'cotisations:history' 'paiement' paiement.id %}">
|
||||
<a class="btn btn-info btn-sm" role="button" title="{% trans "Historique" %}" href="{% url 'cotisations:history' 'paiement' paiement.id %}">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -25,13 +25,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Controle des factures{% endblock %}
|
||||
{% block title %}{% trans "Invoice control" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Controle et validité des factures</h2>
|
||||
<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">
|
||||
{% csrf_token %}
|
||||
|
@ -39,24 +40,50 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Profil</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='name' text='Nom' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='surname' text='Prénom' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='id' text='Id facture' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='user-id' text='Id user' %}</th>
|
||||
<th>Designation</th>
|
||||
<th>Prix total</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='paiement' text='Moyen de paiement' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='date' text='Date' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='valid' text='Valide' %}</th>
|
||||
<th>{% include "buttons/sort.html" with prefix='control' col='control' text='Contrôlée' %}</th>
|
||||
<th>{% trans "Profil" %}</th>
|
||||
<th>
|
||||
{% trans "Last name" as tr_last_name %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='name' text=tr_last_name %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "First name" as tr_first_name %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='surname' text=tr_first_name %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Invoice id" as tr_invoice_id %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='id' text=tr_invoice_id %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "User id" as tr_user_id %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='user-id' text=tr_user_id %}
|
||||
</th>
|
||||
<th>{% trans "Designation" %}</th>
|
||||
<th>{% trans "Total price" %}</th>
|
||||
<th>
|
||||
{% trans "Payment method" as tr_payment_method %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='paiement' text=tr_payment_method %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Date" as tr_date %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='date' text=tr_date %}<
|
||||
/th>
|
||||
<th>
|
||||
{% trans "Validated" as tr_validated %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='valid' text=tr_validated %}<
|
||||
/th>
|
||||
<th>
|
||||
{% trans "Controlled" as tr_controlled %}
|
||||
{% include 'buttons/sort.html' with prefix='control' col='control' text=tr_controlled %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for form in controlform.forms %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<tr>
|
||||
|
||||
<td><a href="{% url "users:profil" form.instance.user.id%}" class="btn btn-primary btn-sm" role="button"><i class="fa fa-user"></i></a>
|
||||
<td>
|
||||
<a href="{% url 'users:profil' form.instance.user.id%}" class="btn btn-primary btn-sm" role="button">
|
||||
<i class="fa fa-user"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ form.instance.user.name }}</td>
|
||||
<td>{{ form.instance.user.surname }}</td>
|
||||
|
@ -74,10 +101,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% bootstrap_button "Modifier" button_type="submit" icon="star" %}
|
||||
{% trans "Edit" as tr_edit %}
|
||||
{% bootstrap_button tr_edit button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
{% if facture_list.paginator %}
|
||||
{% include "pagination.html" with list=facture_list %}
|
||||
{% include 'pagination.html' with list=facture_list %}
|
||||
{% endif %}
|
||||
|
|
|
@ -24,17 +24,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Création et modification de machines{% endblock %}
|
||||
{% block title %}{% trans "Deletion of cotisations" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<h4>Attention, voulez-vous vraiment supprimer cet objet {{ objet_name }} ( {{ objet }} ) ?</h4>
|
||||
{% bootstrap_button "Confirmer" button_type="submit" icon="trash" %}
|
||||
<h4>
|
||||
{% blocktrans %}
|
||||
Warning. Are you sure you really want te delete this {{ object_name }} object ( {{ objet }} ) ?
|
||||
{% endblocktrans %}
|
||||
</h4>
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type='submit' icon='trash' %}
|
||||
</form>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
{% endblock %}
|
||||
|
|
|
@ -26,23 +26,24 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load massive_bootstrap_form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Création et modification de factures{% endblock %}
|
||||
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors factureform %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>Editer la facture</h3>
|
||||
<h3>{% trans "Edit the invoice" %}</h3>
|
||||
{% massive_bootstrap_form factureform 'user' %}
|
||||
{{ venteform.management_form }}
|
||||
<h3>Articles de la facture</h3>
|
||||
<h3>{% trans "Invoice's articles" %}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Désignation</th>
|
||||
<th>Quantité</th>
|
||||
<th>{% trans "Designation" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for form in venteform.forms %}
|
||||
|
@ -56,7 +57,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -25,8 +25,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Création et modification de factures{% endblock %}
|
||||
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors factureform %}
|
||||
|
@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form factureform %}
|
||||
{% bootstrap_button action_name button_type="submit" icon="star" %}
|
||||
{% bootstrap_button action_name button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -24,14 +24,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Facture{% endblock %}
|
||||
{% block title %}{% trans "Invoices" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Cotisations</h2>
|
||||
{% include "cotisations/aff_cotisations.html" with facture_list=facture_list %}
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>{% trans "Cotisations" %}</h2>
|
||||
{% include 'cotisations/aff_cotisations.html' with facture_list=facture_list %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -25,18 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Articles{% endblock %}
|
||||
{% block title %}{% trans "Articles" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Liste des types d'articles</h2>
|
||||
<h2>{% trans "Article types list" %}</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> Ajouter un type d'articles</a>
|
||||
<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" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-article' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs types d'articles</a>
|
||||
{% include "cotisations/aff_article.html" with article_list=article_list %}
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-article' %}">
|
||||
<i class="fa fa-trash"></i> {% trans "Delete article types" %}
|
||||
</a>
|
||||
{% include 'cotisations/aff_article.html' with article_list=article_list %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -25,18 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Banques{% endblock %}
|
||||
{% block title %}{% trans "Banks" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Liste des banques</h2>
|
||||
<h2>{% trans "Banks list" %}</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> Ajouter une banque</a>
|
||||
<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" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-banque' %}"><i class="fa fa-trash"></i> Supprimer une ou plusieurs banques</a>
|
||||
{% include "cotisations/aff_banque.html" with banque_list=banque_list %}
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-banque' %}">
|
||||
<i class="fa fa-trash"></i> {% trans "Delete banks" %}
|
||||
</a>
|
||||
{% include 'cotisations/aff_banque.html' with banque_list=banque_list %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -25,18 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Paiements{% endblock %}
|
||||
{% block title %}{% trans "Payments" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Liste des types de paiements</h2>
|
||||
<h2>{% trans "Payment types list" %}</h2>
|
||||
{% can_create Paiement %}
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-paiement' %}"><i class="fa fa-cart-plus"></i> Ajouter un type de paiement</a>
|
||||
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:add-paiement' %}">
|
||||
<i class="fa fa-cart-plus"></i> {% trans "Add a payment type" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-paiement' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs types de paiements</a>
|
||||
{% include "cotisations/aff_paiement.html" with paiement_list=paiement_list %}
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-paiement' %}">
|
||||
<i class="fa fa-trash"></i> {% trans "Delete payment types" %}
|
||||
</a>
|
||||
{% include 'cotisations/aff_paiement.html' with paiement_list=paiement_list %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -25,40 +25,45 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Création et modification de factures{% endblock %}
|
||||
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors factureform %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>Nouvelle facture</h3>
|
||||
<h3>{% trans "New invoice" %}</h3>
|
||||
<p>
|
||||
Solde de l'utilisateur : {{ user.solde }} €
|
||||
{% blocktrans %}
|
||||
User's balance : {{ user.solde }} €
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_form factureform %}
|
||||
{{ venteform.management_form }}
|
||||
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
|
||||
<h3>Articles de la facture</h3>
|
||||
<h3>{% trans "Invoice's articles" %}</h3>
|
||||
<div id="form_set" class="form-group">
|
||||
{% for form in venteform.forms %}
|
||||
<div class='product_to_sell form-inline'>
|
||||
Article :
|
||||
{% trans "Article" %} :
|
||||
{% bootstrap_form form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm"
|
||||
id="id_form-0-article-remove" type="button">
|
||||
<button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un article" id="add_one">
|
||||
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
|
||||
<p>
|
||||
Prix total : <span id="total_price">0,00</span> €
|
||||
{% blocktrans %}
|
||||
Total price : <span id="total_price">0,00</span> €
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_button "Créer" button_type="submit" icon="star" %}
|
||||
{% trans "Create" as tr_create %}
|
||||
{% bootstrap_button tr_create button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
{% extends "cotisations/sidebar.html" %}
|
||||
{% comment %}
|
||||
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||
|
@ -26,36 +25,39 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Création et modification de factures{% endblock %}
|
||||
{% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% bootstrap_form_errors venteform.management_form %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>Nouvelle facture</h3>
|
||||
<h3>{% trans "New invoice" %}</h3>
|
||||
{{ venteform.management_form }}
|
||||
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
|
||||
<h3>Articles de la facture</h3>
|
||||
<h3>{% trans "Invoice's articles" %}</h3>
|
||||
<div id="form_set" class="form-group">
|
||||
{% for form in venteform.forms %}
|
||||
<div class='product_to_sell form-inline'>
|
||||
Article :
|
||||
{% trans "Article" %} :
|
||||
{% bootstrap_form form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm"
|
||||
id="id_form-0-article-remove" type="button">
|
||||
<button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un article" id="add_one">
|
||||
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add an article"%}" id="add_one">
|
||||
<p>
|
||||
Prix total : <span id="total_price">0,00</span> €
|
||||
{% blocktrans %}
|
||||
Total price : <span id="total_price">0,00</span> €
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type='submit' icon='star' %}
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
@ -68,8 +70,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
var template = `Article :
|
||||
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
|
||||
|
||||
<button class="btn btn-danger btn-sm"
|
||||
id="id_form-__prefix__-article-remove" type="button">
|
||||
<button class="btn btn-danger btn-sm" id="id_form-__prefix__-article-remove" type="button">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>`
|
||||
|
||||
|
|
|
@ -25,13 +25,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Rechargement du solde{% endblock %}
|
||||
{% block title %}{% trans "Balance refill" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Recharger de {{ amount }} €</h3>
|
||||
<form class="form" method="{{ method }}" action="{{action}}">
|
||||
<h3>
|
||||
{% blocktrans %}
|
||||
Refill of {{ amount }} €
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<form class="form" method="{{ method }}" action="{{ action }}">
|
||||
{{ content | safe }}
|
||||
{% bootstrap_button "Payer" button_type="submit" icon="piggy-bank" %}
|
||||
</form>
|
||||
{% trans "Pay" as tr_pay %}
|
||||
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -25,15 +25,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles%}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Rechargement du solde{% endblock %}
|
||||
{% block title %}{% trans "Balance refill" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Rechargement du solde</h2>
|
||||
<h3>Solde : <span class="label label-default">{{ request.user.solde }} €</span></h3>
|
||||
<form class="form" method="post">
|
||||
<h2>{% trans "Balance refill" %}</h2>
|
||||
<h3>
|
||||
{% blocktrans %}
|
||||
Balance : <span class="label label-default">{{ request.user.solde }} €</span>
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form rechargeform %}
|
||||
{% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %}
|
||||
</form>
|
||||
{% trans "Confirm" as tr_confirm %}
|
||||
{% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -24,40 +24,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load acl %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% can_change Facture pdf %}
|
||||
<a class="list-group-item list-group-item-success" href="{% url "cotisations:new-facture-pdf" %}">
|
||||
<i class="fa fa-plus"></i>
|
||||
Créer une facture
|
||||
<i class="fa fa-plus"></i> {% trans "Create an invoice" %}
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}">
|
||||
<i class="fa fa-eye"></i>
|
||||
Contrôler les factures
|
||||
<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" %}">
|
||||
<i class="fa fa-list-ul"></i>
|
||||
Factures
|
||||
<i class="fa fa-list-ul"></i> {% trans "Invoices" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% can_view_all Article %}
|
||||
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
|
||||
<i class="fa fa-list-ul"></i>
|
||||
Articles en vente
|
||||
<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" %}">
|
||||
<i class="fa fa-list-ul"></i>
|
||||
Banques
|
||||
<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" %}">
|
||||
<i class="fa fa-list-ul"></i>
|
||||
Moyens de paiement
|
||||
<i class="fa fa-list-ul"></i> {% trans "Payment methods" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
# 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.
|
||||
"""tex.py
|
||||
Module in charge of rendering some LaTex templates.
|
||||
Used to generated PDF invoice.
|
||||
"""
|
||||
|
||||
from django.template.loader import get_template
|
||||
from django.template import Context
|
||||
|
@ -37,6 +41,10 @@ CACHE_TIMEOUT = getattr(settings, 'TEX_CACHE_TIMEOUT', 86400) # 1 day
|
|||
|
||||
|
||||
def render_invoice(request, ctx={}):
|
||||
"""
|
||||
Render an invoice using some available information such as the current
|
||||
date, the user, the articles, the prices, ...
|
||||
"""
|
||||
filename = '_'.join([
|
||||
'invoice',
|
||||
slugify(ctx['asso_name']),
|
||||
|
@ -50,6 +58,11 @@ def render_invoice(request, ctx={}):
|
|||
return r
|
||||
|
||||
def render_tex(request, template, ctx={}):
|
||||
"""
|
||||
Creates a PDF from a LaTex templates using pdflatex.
|
||||
Writes it in a temporary directory and send back an HTTP response for
|
||||
accessing this file.
|
||||
"""
|
||||
context = Context(ctx)
|
||||
template = get_template(template)
|
||||
rendered_tpl = template.render(context).encode('utf-8')
|
||||
|
|
|
@ -36,6 +36,7 @@ from django.db import transaction
|
|||
from django.db.models import Q
|
||||
from django.forms import modelformset_factory, formset_factory
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.debug import sensitive_variables
|
||||
# Import des models, forms et fonctions re2o
|
||||
|
@ -72,7 +73,7 @@ from .forms import (
|
|||
NewFactureSoldeForm,
|
||||
RechargeForm
|
||||
)
|
||||
from . import payment
|
||||
from . import payment as online_payment
|
||||
from .tex import render_invoice
|
||||
|
||||
|
||||
|
@ -81,114 +82,141 @@ from .tex import render_invoice
|
|||
@can_create(Facture)
|
||||
@can_edit(User)
|
||||
def new_facture(request, user, userid):
|
||||
"""Creation d'une facture pour un user. Renvoie la liste des articles
|
||||
et crée des factures dans un formset. Utilise un peu de js coté template
|
||||
pour ajouter des articles.
|
||||
Parse les article et boucle dans le formset puis save les ventes,
|
||||
enfin sauve la facture parente.
|
||||
TODO : simplifier cette fonction, déplacer l'intelligence coté models
|
||||
Facture et Vente."""
|
||||
facture = Facture(user=user)
|
||||
# Le template a besoin de connaitre les articles pour le js
|
||||
"""
|
||||
View called to create a new invoice.
|
||||
Currently, Send the list of available articles for the user along with
|
||||
a formset of a new invoice (based on the `:forms:NewFactureForm()` form.
|
||||
A bit of JS is used in the template to add articles in a fancier way.
|
||||
If everything is correct, save each one of the articles, save the
|
||||
purchase object associated and finally the newly created invoice.
|
||||
|
||||
TODO : The whole verification process should be moved to the model. This
|
||||
function should only act as a dumb interface between the model and the
|
||||
user.
|
||||
"""
|
||||
invoice = Facture(user=user)
|
||||
# The template needs the list of articles (for the JS part)
|
||||
article_list = Article.objects.filter(
|
||||
Q(type_user='All') | Q(type_user=request.user.class_name)
|
||||
)
|
||||
# On envoie la form fature et un formset d'articles
|
||||
facture_form = NewFactureForm(request.POST or None, instance=facture)
|
||||
# Building the invocie form and the article formset
|
||||
invoice_form = NewFactureForm(request.POST or None, instance=invoice)
|
||||
if request.user.is_class_club:
|
||||
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
|
||||
else:
|
||||
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
|
||||
if facture_form.is_valid() and article_formset.is_valid():
|
||||
new_facture_instance = facture_form.save(commit=False)
|
||||
|
||||
if invoice_form.is_valid() and article_formset.is_valid():
|
||||
new_invoice_instance = invoice_form.save(commit=False)
|
||||
articles = article_formset
|
||||
# Si au moins un article est rempli
|
||||
# Check if at leat one article has been selected
|
||||
if any(art.cleaned_data for art in articles):
|
||||
user_solde = OptionalUser.get_cached_value('user_solde')
|
||||
solde_negatif = OptionalUser.get_cached_value('solde_negatif')
|
||||
# Si on paye par solde, que l'option est activée,
|
||||
# on vérifie que le négatif n'est pas atteint
|
||||
if user_solde:
|
||||
if new_facture_instance.paiement == Paiement.objects.get_or_create(
|
||||
user_balance = OptionalUser.get_cached_value('user_solde')
|
||||
negative_balance = OptionalUser.get_cached_value('solde_negatif')
|
||||
# If the paiement using balance has been activated,
|
||||
# checking that the total price won't get the user under
|
||||
# the authorized minimum (negative_balance)
|
||||
if user_balance:
|
||||
# TODO : change Paiement to Payment
|
||||
if new_invoice_instance.paiement == Paiement.objects.get_or_create(
|
||||
moyen='solde'
|
||||
)[0]:
|
||||
prix_total = 0
|
||||
total_price = 0
|
||||
for art_item in articles:
|
||||
if art_item.cleaned_data:
|
||||
prix_total += art_item.cleaned_data['article']\
|
||||
total_price += art_item.cleaned_data['article']\
|
||||
.prix*art_item.cleaned_data['quantity']
|
||||
if float(user.solde) - float(prix_total) < solde_negatif:
|
||||
messages.error(request, "Le solde est insuffisant pour\
|
||||
effectuer l'opération")
|
||||
if float(user.solde) - float(total_price) < negative_balance:
|
||||
messages.error(
|
||||
request,
|
||||
_("Your balance is too low for this operation.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': userid}
|
||||
))
|
||||
new_facture_instance.save()
|
||||
# Saving the invoice
|
||||
new_invoice_instance.save()
|
||||
|
||||
# Building a purchase for each article sold
|
||||
for art_item in articles:
|
||||
if art_item.cleaned_data:
|
||||
article = art_item.cleaned_data['article']
|
||||
quantity = art_item.cleaned_data['quantity']
|
||||
new_vente = Vente.objects.create(
|
||||
facture=new_facture_instance,
|
||||
new_purchase = Vente.objects.create(
|
||||
facture=new_invoice_instance,
|
||||
name=article.name,
|
||||
prix=article.prix,
|
||||
type_cotisation=article.type_cotisation,
|
||||
duration=article.duration,
|
||||
number=quantity
|
||||
)
|
||||
new_vente.save()
|
||||
new_purchase.save()
|
||||
|
||||
# In case a cotisation was bought, inform the user, the
|
||||
# cotisation time has been extended too
|
||||
if any(art_item.cleaned_data['article'].type_cotisation
|
||||
for art_item in articles if art_item.cleaned_data):
|
||||
messages.success(
|
||||
request,
|
||||
"La cotisation a été prolongée\
|
||||
pour l'adhérent %s jusqu'au %s" % (
|
||||
user.pseudo, user.end_adhesion()
|
||||
)
|
||||
_("The cotisation of %(member_name)s has been \
|
||||
extended to %(end_date)s.") % {
|
||||
member_name: user.pseudo,
|
||||
end_date: user.end_adhesion()
|
||||
}
|
||||
)
|
||||
# Else, only tell the invoice was created
|
||||
else:
|
||||
messages.success(request, "La facture a été crée")
|
||||
messages.success(
|
||||
request,
|
||||
_("The invoice has been created.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': userid}
|
||||
))
|
||||
messages.error(
|
||||
request,
|
||||
u"Il faut au moins un article valide pour créer une facture"
|
||||
_("You need to choose at least one article.")
|
||||
)
|
||||
return form({
|
||||
'factureform': facture_form,
|
||||
return form(
|
||||
{
|
||||
'factureform': invoice_form,
|
||||
'venteform': article_formset,
|
||||
'articlelist': article_list
|
||||
}, 'cotisations/new_facture.html', request)
|
||||
},
|
||||
'cotisations/new_facture.html', request
|
||||
)
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
@login_required
|
||||
@can_change(Facture, 'pdf')
|
||||
def new_facture_pdf(request):
|
||||
"""Permet de générer un pdf d'une facture. Réservée
|
||||
au trésorier, permet d'emettre des factures sans objet
|
||||
Vente ou Facture correspondant en bdd"""
|
||||
facture_form = NewFactureFormPdf(request.POST or None)
|
||||
if facture_form.is_valid():
|
||||
"""
|
||||
View used to generate a custom PDF invoice. It's mainly used to
|
||||
get invoices that are not taken into account, for the administrative
|
||||
point of view.
|
||||
"""
|
||||
invoice_form = NewFactureFormPdf(request.POST or None)
|
||||
if invoice_form.is_valid():
|
||||
tbl = []
|
||||
article = facture_form.cleaned_data['article']
|
||||
quantite = facture_form.cleaned_data['number']
|
||||
quantity = facture_form.cleaned_data['number']
|
||||
paid = facture_form.cleaned_data['paid']
|
||||
destinataire = facture_form.cleaned_data['dest']
|
||||
chambre = facture_form.cleaned_data['chambre']
|
||||
fid = facture_form.cleaned_data['fid']
|
||||
recipient = facture_form.cleaned_data['dest']
|
||||
room = facture_form.cleaned_data['chambre']
|
||||
invoice_id = facture_form.cleaned_data['fid']
|
||||
for art in article:
|
||||
tbl.append([art, quantite, art.prix * quantite])
|
||||
prix_total = sum(a[2] for a in tbl)
|
||||
user = {'name': destinataire, 'room': chambre}
|
||||
tbl.append([art, quantity, art.prix * quantity])
|
||||
total_price = sum(a[2] for a in tbl)
|
||||
user = {'name': recipient, 'room': room}
|
||||
return render_invoice(request, {
|
||||
'DATE': timezone.now(),
|
||||
'dest': user,
|
||||
'fid': fid,
|
||||
'fid': invoice_id,
|
||||
'article': tbl,
|
||||
'total': prix_total,
|
||||
'total': total_price,
|
||||
'paid': paid,
|
||||
'asso_name': AssoOption.get_cached_value('name'),
|
||||
'line1': AssoOption.get_cached_value('adresse1'),
|
||||
|
@ -199,29 +227,32 @@ def new_facture_pdf(request):
|
|||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
||||
})
|
||||
return form({
|
||||
'factureform': facture_form,
|
||||
'action_name' : 'Editer'
|
||||
'factureform': invoice_form,
|
||||
'action_name': _("Edit")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
@login_required
|
||||
@can_view(Facture)
|
||||
def facture_pdf(request, facture, factureid):
|
||||
"""Affiche en pdf une facture. Cree une ligne par Vente de la facture,
|
||||
et génére une facture avec le total, le moyen de paiement, l'adresse
|
||||
de l'adhérent, etc. Réservée à self pour un user sans droits,
|
||||
les droits cableurs permettent d'afficher toute facture"""
|
||||
|
||||
ventes_objects = Vente.objects.all().filter(facture=facture)
|
||||
ventes = []
|
||||
for vente in ventes_objects:
|
||||
ventes.append([vente, vente.number, vente.prix_total])
|
||||
"""
|
||||
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=facture)
|
||||
purchases = []
|
||||
for purchase in purchases_objects:
|
||||
purchases.append([purchase, purchase.number, purchase.prix_total])
|
||||
return render_invoice(request, {
|
||||
'paid': True,
|
||||
'fid': facture.id,
|
||||
'DATE': facture.date,
|
||||
'dest': facture.user,
|
||||
'article': ventes,
|
||||
'article': purchases,
|
||||
'total': facture.prix_total(),
|
||||
'asso_name': AssoOption.get_cached_value('name'),
|
||||
'line1': AssoOption.get_cached_value('adresse1'),
|
||||
|
@ -233,302 +264,434 @@ def facture_pdf(request, facture, factureid):
|
|||
})
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
@login_required
|
||||
@can_edit(Facture)
|
||||
def edit_facture(request, facture, factureid):
|
||||
"""Permet l'édition d'une facture. On peut y éditer les ventes
|
||||
déjà effectuer, ou rendre une facture invalide (non payées, chèque
|
||||
en bois etc). Mets à jour les durée de cotisation attenantes"""
|
||||
facture_form = EditFactureForm(request.POST or None, instance=facture, user=request.user)
|
||||
ventes_objects = Vente.objects.filter(facture=facture)
|
||||
vente_form_set = modelformset_factory(
|
||||
"""
|
||||
View used to edit an existing invoice.
|
||||
Articles can be added or remove to the invoice and quantity
|
||||
can be set as desired. This is also the view used to invalidate
|
||||
an invoice.
|
||||
"""
|
||||
invoice_form = EditFactureForm(request.POST or None, instance=facture, user=request.user)
|
||||
purchases_objects = Vente.objects.filter(facture=facture)
|
||||
purchase_form_set = modelformset_factory(
|
||||
Vente,
|
||||
fields=('name', 'number'),
|
||||
extra=0,
|
||||
max_num=len(ventes_objects)
|
||||
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 invoice has been successfully edited.")
|
||||
)
|
||||
vente_form = vente_form_set(request.POST or None, queryset=ventes_objects)
|
||||
if facture_form.is_valid() and vente_form.is_valid():
|
||||
if facture_form.changed_data:
|
||||
facture_form.save()
|
||||
vente_form.save()
|
||||
messages.success(request, "La facture a bien été modifiée")
|
||||
return redirect(reverse('cotisations:index'))
|
||||
return form({
|
||||
'factureform': facture_form,
|
||||
'venteform': vente_form
|
||||
'factureform': invoice_form,
|
||||
'venteform': purchase_form
|
||||
}, 'cotisations/edit_facture.html', request)
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
@login_required
|
||||
@can_delete(Facture)
|
||||
def del_facture(request, facture, factureid):
|
||||
"""Suppression d'une facture. Supprime en cascade les ventes
|
||||
et cotisations filles"""
|
||||
"""
|
||||
View used to delete an existing invocie.
|
||||
"""
|
||||
if request.method == "POST":
|
||||
facture.delete()
|
||||
messages.success(request, "La facture a été détruite")
|
||||
messages.success(
|
||||
request,
|
||||
_("The invoice has been successfully deleted.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index'))
|
||||
return form({
|
||||
'objet': facture,
|
||||
'objet_name': 'facture'
|
||||
'objet_name': _("Invoice")
|
||||
}, 'cotisations/delete.html', request)
|
||||
|
||||
|
||||
# TODO : change solde to balance
|
||||
@login_required
|
||||
@can_create(Facture)
|
||||
@can_edit(User)
|
||||
def credit_solde(request, user, userid):
|
||||
""" Credit ou débit de solde """
|
||||
facture = CreditSoldeForm(request.POST or None)
|
||||
if facture.is_valid():
|
||||
facture_instance = facture.save(commit=False)
|
||||
facture_instance.user = user
|
||||
facture_instance.save()
|
||||
new_vente = Vente.objects.create(
|
||||
facture=facture_instance,
|
||||
"""
|
||||
View used to edit the balance of a user.
|
||||
Can be use either to increase or decrease a user's balance.
|
||||
"""
|
||||
# TODO : change facture to invoice
|
||||
invoice = CreditSoldeForm(request.POST or None)
|
||||
if invoice.is_valid():
|
||||
invoice_instance = invoice.save(commit=False)
|
||||
invoice_instance.user = user
|
||||
invoice_instance.save()
|
||||
new_purchase = Vente.objects.create(
|
||||
facture=invoice_instance,
|
||||
name="solde",
|
||||
prix=facture.cleaned_data['montant'],
|
||||
prix=invoice.cleaned_data['montant'],
|
||||
number=1
|
||||
)
|
||||
new_vente.save()
|
||||
messages.success(request, "Solde modifié")
|
||||
new_purchase.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("Balance successfully updated.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index'))
|
||||
return form({'factureform': facture, 'action_name' : 'Créditer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': facture,
|
||||
'action_name': _("Edit")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
@can_create(Article)
|
||||
def add_article(request):
|
||||
"""Ajoute un article. Champs : désignation,
|
||||
prix, est-ce une cotisation et si oui sa durée
|
||||
Réservé au trésorier
|
||||
Nota bene : les ventes déjà effectuées ne sont pas reliées
|
||||
aux articles en vente. La désignation, le prix... sont
|
||||
copiés à la création de la facture. Un changement de prix n'a
|
||||
PAS de conséquence sur les ventes déjà faites"""
|
||||
"""
|
||||
View used to add an article.
|
||||
|
||||
.. note:: If a purchase has already been sold, the price are calculated
|
||||
once and for all. That means even if the price of an article is edited
|
||||
later, it won't change the invoice. That is really important to keep
|
||||
this behaviour in order not to modify all the past and already
|
||||
accepted invoices.
|
||||
"""
|
||||
article = ArticleForm(request.POST or None)
|
||||
if article.is_valid():
|
||||
article.save()
|
||||
messages.success(request, "L'article a été ajouté")
|
||||
messages.success(
|
||||
request,
|
||||
_("The article has been successfully created.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-article'))
|
||||
return form({'factureform': article, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': article,
|
||||
'action_name': _("Add")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
@can_edit(Article)
|
||||
def edit_article(request, article_instance, articleid):
|
||||
"""Edition d'un article (designation, prix, etc)
|
||||
Réservé au trésorier"""
|
||||
"""
|
||||
View used to edit an article.
|
||||
"""
|
||||
article = ArticleForm(request.POST or None, instance=article_instance)
|
||||
if article.is_valid():
|
||||
if article.changed_data:
|
||||
article.save()
|
||||
messages.success(request, "Type d'article modifié")
|
||||
messages.success(
|
||||
request,
|
||||
_("The article has been successfully edited.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-article'))
|
||||
return form({'factureform': article, 'action_name' : 'Editer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': article,
|
||||
'action_name': _('Edit')
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
@can_delete_set(Article)
|
||||
def del_article(request, instances):
|
||||
"""Suppression d'un article en vente"""
|
||||
"""
|
||||
View used to delete one of the articles.
|
||||
"""
|
||||
article = DelArticleForm(request.POST or None, instances=instances)
|
||||
if article.is_valid():
|
||||
article_del = article.cleaned_data['articles']
|
||||
article_del.delete()
|
||||
messages.success(request, "Le/les articles ont été supprimé")
|
||||
messages.success(
|
||||
request,
|
||||
_("The article(s) have been successfully deleted.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-article'))
|
||||
return form({'factureform': article, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': article,
|
||||
'action_name': _("Delete")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change paiement to payment
|
||||
@login_required
|
||||
@can_create(Paiement)
|
||||
def add_paiement(request):
|
||||
"""Ajoute un moyen de paiement. Relié aux factures
|
||||
via foreign key"""
|
||||
paiement = PaiementForm(request.POST or None)
|
||||
if paiement.is_valid():
|
||||
paiement.save()
|
||||
messages.success(request, "Le moyen de paiement a été ajouté")
|
||||
"""
|
||||
View used to add a payment method.
|
||||
"""
|
||||
payment = PaiementForm(request.POST or None)
|
||||
if payment.is_valid():
|
||||
payment.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("The payment method has been successfully created.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-paiement'))
|
||||
return form({'factureform': paiement, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': payment,
|
||||
'action_name': _("Add")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : chnage paiement to Payment
|
||||
@login_required
|
||||
@can_edit(Paiement)
|
||||
def edit_paiement(request, paiement_instance, paiementid):
|
||||
"""Edition d'un moyen de paiement"""
|
||||
paiement = PaiementForm(request.POST or None, instance=paiement_instance)
|
||||
if paiement.is_valid():
|
||||
if paiement.changed_data:
|
||||
paiement.save()
|
||||
messages.success(request, "Type de paiement modifié")
|
||||
"""
|
||||
View used to edit a payment method.
|
||||
"""
|
||||
payment = PaiementForm(request.POST or None, instance=paiement_instance)
|
||||
if payment.is_valid():
|
||||
if payment.changed_data:
|
||||
payment.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("The payement method has been successfully edited.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-paiement'))
|
||||
return form({'factureform': paiement, 'action_name' : 'Editer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': payment,
|
||||
'action_name': _("Edit")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change paiement to payment
|
||||
@login_required
|
||||
@can_delete_set(Paiement)
|
||||
def del_paiement(request, instances):
|
||||
"""Suppression d'un moyen de paiement"""
|
||||
paiement = DelPaiementForm(request.POST or None, instances=instances)
|
||||
if paiement.is_valid():
|
||||
paiement_dels = paiement.cleaned_data['paiements']
|
||||
for paiement_del in paiement_dels:
|
||||
"""
|
||||
View used to delete a set of payment methods.
|
||||
"""
|
||||
payment = DelPaiementForm(request.POST or None, instances=instances)
|
||||
if payment.is_valid():
|
||||
payment_dels = payment.cleaned_data['paiements']
|
||||
for payment_del in payment_dels:
|
||||
try:
|
||||
paiement_del.delete()
|
||||
payment_del.delete()
|
||||
messages.success(
|
||||
request,
|
||||
"Le moyen de paiement a été supprimé"
|
||||
_("The payment method %(method_name)s has been \
|
||||
successfully deleted.") % {
|
||||
method_name: payment_del
|
||||
}
|
||||
)
|
||||
except ProtectedError:
|
||||
messages.error(
|
||||
request,
|
||||
"Le moyen de paiement %s est affecté à au moins une\
|
||||
facture, vous ne pouvez pas le supprimer" % paiement_del
|
||||
_("The payment method %(method_name)s can't be deleted \
|
||||
because there are invoices using it.") % {
|
||||
method_name: payment_del
|
||||
}
|
||||
)
|
||||
return redirect(reverse('cotisations:index-paiement'))
|
||||
return form({'factureform': paiement, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': payment,
|
||||
'action_name': _("Delete")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
@login_required
|
||||
@can_create(Banque)
|
||||
def add_banque(request):
|
||||
"""Ajoute une banque à la liste des banques"""
|
||||
banque = BanqueForm(request.POST or None)
|
||||
if banque.is_valid():
|
||||
banque.save()
|
||||
messages.success(request, "La banque a été ajoutée")
|
||||
"""
|
||||
View used to add a bank.
|
||||
"""
|
||||
bank = BanqueForm(request.POST or None)
|
||||
if bank.is_valid():
|
||||
bank.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("The bank has been successfully created.")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-banque'))
|
||||
return form({'factureform': banque, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': bank,
|
||||
'action_name': _("Add")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
@login_required
|
||||
@can_edit(Banque)
|
||||
def edit_banque(request, banque_instance, banqueid):
|
||||
"""Edite le nom d'une banque"""
|
||||
banque = BanqueForm(request.POST or None, instance=banque_instance)
|
||||
if banque.is_valid():
|
||||
if banque.changed_data:
|
||||
banque.save()
|
||||
messages.success(request, "Banque modifiée")
|
||||
"""
|
||||
View used to edit a bank.
|
||||
"""
|
||||
bank = BanqueForm(request.POST or None, instance=banque_instance)
|
||||
if bank.is_valid():
|
||||
if bank.changed_data:
|
||||
bank.save()
|
||||
messages.success(
|
||||
request,
|
||||
_("The bank has been successfully edited")
|
||||
)
|
||||
return redirect(reverse('cotisations:index-banque'))
|
||||
return form({'factureform': banque, 'action_name' : 'Editer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': bank,
|
||||
'action_name': _("Edit")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : chnage banque to bank
|
||||
@login_required
|
||||
@can_delete_set(Banque)
|
||||
def del_banque(request, instances):
|
||||
"""Supprime une banque"""
|
||||
banque = DelBanqueForm(request.POST or None, instances=instances)
|
||||
if banque.is_valid():
|
||||
banque_dels = banque.cleaned_data['banques']
|
||||
for banque_del in banque_dels:
|
||||
"""
|
||||
View used to delete a set of banks.
|
||||
"""
|
||||
bank = DelBanqueForm(request.POST or None, instances=instances)
|
||||
if bank.is_valid():
|
||||
bank_dels = bank.cleaned_data['banques']
|
||||
for bank_del in bank_dels:
|
||||
try:
|
||||
banque_del.delete()
|
||||
messages.success(request, "La banque a été supprimée")
|
||||
bank_del.delete()
|
||||
messages.success(
|
||||
request,
|
||||
_("The bank %(bank_name)s has been successfully \
|
||||
deleted.") % {
|
||||
bank_name: bank_del
|
||||
}
|
||||
)
|
||||
except ProtectedError:
|
||||
messages.error(request, "La banque %s est affectée à au moins\
|
||||
une facture, vous ne pouvez pas la supprimer" % banque_del)
|
||||
messages.error(
|
||||
request,
|
||||
_("The bank %(bank_name)s can't be deleted \
|
||||
because there are invoices using it.") % {
|
||||
bank_name: bank_del
|
||||
}
|
||||
)
|
||||
return redirect(reverse('cotisations:index-banque'))
|
||||
return form({'factureform': banque, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request)
|
||||
return form({
|
||||
'factureform': bank,
|
||||
'action_name': _("Delete")
|
||||
}, 'cotisations/facture.html', request)
|
||||
|
||||
|
||||
# TODO : change facture to invoice
|
||||
@login_required
|
||||
@can_view_all(Facture)
|
||||
@can_change(Facture, 'control')
|
||||
def control(request):
|
||||
"""Pour le trésorier, vue pour controler en masse les
|
||||
factures.Case à cocher, pratique"""
|
||||
"""
|
||||
View used to control the invoices all at once.
|
||||
"""
|
||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||
facture_list = Facture.objects.select_related('user').select_related('paiement')
|
||||
facture_list = SortTable.sort(
|
||||
facture_list,
|
||||
invoice_list = Facture.objects.select_related('user').select_related('paiement')
|
||||
invoice_list = SortTable.sort(
|
||||
invoice_list,
|
||||
request.GET.get('col'),
|
||||
request.GET.get('order'),
|
||||
SortTable.COTISATIONS_CONTROL
|
||||
)
|
||||
controlform_set = modelformset_factory(
|
||||
control_invoices_formset = modelformset_factory(
|
||||
Facture,
|
||||
fields=('control', 'valid'),
|
||||
extra=0
|
||||
)
|
||||
facture_list = re2o_paginator(request, facture_list, pagination_number)
|
||||
controlform = controlform_set(request.POST or None, queryset=facture_list.object_list)
|
||||
if controlform.is_valid():
|
||||
controlform.save()
|
||||
invoice_list = re2o_paginator(request, invoice_list, pagination_number)
|
||||
control_invoices_form = control_invoices_formset(
|
||||
request.POST or None,
|
||||
queryset=invoice_list.object_list
|
||||
)
|
||||
if control_invoices_form.is_valid():
|
||||
control_invoices_form.save()
|
||||
reversion.set_comment("Controle")
|
||||
return redirect(reverse('cotisations:control'))
|
||||
return render(request, 'cotisations/control.html', {
|
||||
'facture_list': facture_list,
|
||||
'controlform': controlform
|
||||
'facture_list': invoice_list,
|
||||
'controlform': control_invoices_form
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@can_view_all(Article)
|
||||
def index_article(request):
|
||||
"""Affiche l'ensemble des articles en vente"""
|
||||
"""
|
||||
View used to display the list of all available articles.
|
||||
"""
|
||||
# TODO : Offer other means of sorting
|
||||
article_list = Article.objects.order_by('name')
|
||||
return render(request, 'cotisations/index_article.html', {
|
||||
'article_list': article_list
|
||||
})
|
||||
|
||||
|
||||
# TODO : change paiement to payment
|
||||
@login_required
|
||||
@can_view_all(Paiement)
|
||||
def index_paiement(request):
|
||||
"""Affiche l'ensemble des moyens de paiement en vente"""
|
||||
paiement_list = Paiement.objects.order_by('moyen')
|
||||
"""
|
||||
View used to display the list of all available payment methods.
|
||||
"""
|
||||
payment_list = Paiement.objects.order_by('moyen')
|
||||
return render(request, 'cotisations/index_paiement.html', {
|
||||
'paiement_list': paiement_list
|
||||
'paiement_list': payment_list
|
||||
})
|
||||
|
||||
|
||||
# TODO : change banque to bank
|
||||
@login_required
|
||||
@can_view_all(Banque)
|
||||
def index_banque(request):
|
||||
"""Affiche l'ensemble des banques"""
|
||||
banque_list = Banque.objects.order_by('name')
|
||||
"""
|
||||
View used to display the list of all available banks.
|
||||
"""
|
||||
bank_list = Banque.objects.order_by('name')
|
||||
return render(request, 'cotisations/index_banque.html', {
|
||||
'banque_list': banque_list
|
||||
'banque_list': bank_list
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@can_view_all(Facture)
|
||||
def index(request):
|
||||
"""Affiche l'ensemble des factures, pour les cableurs et +"""
|
||||
"""
|
||||
View used to display the list of all exisitng invoices.
|
||||
"""
|
||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||
facture_list = Facture.objects.select_related('user')\
|
||||
invoice_list = Facture.objects.select_related('user')\
|
||||
.select_related('paiement').prefetch_related('vente_set')
|
||||
facture_list = SortTable.sort(
|
||||
facture_list,
|
||||
invoice_list = SortTable.sort(
|
||||
invoice_list,
|
||||
request.GET.get('col'),
|
||||
request.GET.get('order'),
|
||||
SortTable.COTISATIONS_INDEX
|
||||
)
|
||||
facture_list = re2o_paginator(request, facture_list, pagination_number)
|
||||
invoice_list = re2o_paginator(request, invoice_list, pagination_number)
|
||||
return render(request, 'cotisations/index.html', {
|
||||
'facture_list': facture_list
|
||||
'facture_list': invoice_list
|
||||
})
|
||||
|
||||
|
||||
# TODO : merge this function with new_facture() which is nearly the same
|
||||
# TODO : change facture to invoice
|
||||
@login_required
|
||||
def new_facture_solde(request, userid):
|
||||
"""Creation d'une facture pour un user. Renvoie la liste des articles
|
||||
et crée des factures dans un formset. Utilise un peu de js coté template
|
||||
pour ajouter des articles.
|
||||
Parse les article et boucle dans le formset puis save les ventes,
|
||||
enfin sauve la facture parente.
|
||||
TODO : simplifier cette fonction, déplacer l'intelligence coté models
|
||||
Facture et Vente."""
|
||||
"""
|
||||
View called to create a new invoice when using the balance to pay.
|
||||
Currently, send the list of available articles for the user along with
|
||||
a formset of a new invoice (based on the `:forms:NewFactureForm()` form.
|
||||
A bit of JS is used in the template to add articles in a fancier way.
|
||||
If everything is correct, save each one of the articles, save the
|
||||
purchase object associated and finally the newly created invoice.
|
||||
|
||||
TODO : The whole verification process should be moved to the model. This
|
||||
function should only act as a dumb interface between the model and the
|
||||
user.
|
||||
"""
|
||||
user = request.user
|
||||
facture = Facture(user=user)
|
||||
paiement, _created = Paiement.objects.get_or_create(moyen='Solde')
|
||||
facture.paiement = paiement
|
||||
# Le template a besoin de connaitre les articles pour le js
|
||||
invoice = Facture(user=user)
|
||||
payment, _created = Paiement.objects.get_or_create(moyen='Solde')
|
||||
facture.paiement = payment
|
||||
# The template needs the list of articles (for the JS part)
|
||||
article_list = Article.objects.filter(
|
||||
Q(type_user='All') | Q(type_user=request.user.class_name)
|
||||
)
|
||||
|
@ -536,59 +699,74 @@ def new_facture_solde(request, userid):
|
|||
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
|
||||
else:
|
||||
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
|
||||
|
||||
if article_formset.is_valid():
|
||||
articles = article_formset
|
||||
# Si au moins un article est rempli
|
||||
# Check if at leat one article has been selected
|
||||
if any(art.cleaned_data for art in articles):
|
||||
user_solde = OptionalUser.get_cached_value('user_solde')
|
||||
solde_negatif = OptionalUser.get_cached_value('solde_negatif')
|
||||
# Si on paye par solde, que l'option est activée,
|
||||
# on vérifie que le négatif n'est pas atteint
|
||||
if user_solde:
|
||||
prix_total = 0
|
||||
user_balance = OptionalUser.get_cached_value('user_solde')
|
||||
negative_balance = OptionalUser.get_cached_value('solde_negatif')
|
||||
# If the paiement using balance has been activated,
|
||||
# checking that the total price won't get the user under
|
||||
# the authorized minimum (negative_balance)
|
||||
if user_balance:
|
||||
total_price = 0
|
||||
for art_item in articles:
|
||||
if art_item.cleaned_data:
|
||||
prix_total += art_item.cleaned_data['article']\
|
||||
total_price += art_item.cleaned_data['article']\
|
||||
.prix*art_item.cleaned_data['quantity']
|
||||
if float(user.solde) - float(prix_total) < solde_negatif:
|
||||
messages.error(request, "Le solde est insuffisant pour\
|
||||
effectuer l'opération")
|
||||
if float(user.solde) - float(total_price) < negative_balance:
|
||||
messages.error(
|
||||
request,
|
||||
_("The balance is too low for this operation.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': userid}
|
||||
))
|
||||
facture.save()
|
||||
# Saving the invoice
|
||||
invoice.save()
|
||||
|
||||
# Building a purchase for each article sold
|
||||
for art_item in articles:
|
||||
if art_item.cleaned_data:
|
||||
article = art_item.cleaned_data['article']
|
||||
quantity = art_item.cleaned_data['quantity']
|
||||
new_vente = Vente.objects.create(
|
||||
facture=facture,
|
||||
new_purchase = Vente.objects.create(
|
||||
facture=invoice,
|
||||
name=article.name,
|
||||
prix=article.prix,
|
||||
type_cotisation=article.type_cotisation,
|
||||
duration=article.duration,
|
||||
number=quantity
|
||||
)
|
||||
new_vente.save()
|
||||
new_purchase.save()
|
||||
|
||||
# In case a cotisation was bought, inform the user, the
|
||||
# cotisation time has been extended too
|
||||
if any(art_item.cleaned_data['article'].type_cotisation
|
||||
for art_item in articles if art_item.cleaned_data):
|
||||
messages.success(
|
||||
request,
|
||||
"La cotisation a été prolongée\
|
||||
pour l'adhérent %s jusqu'au %s" % (
|
||||
user.pseudo, user.end_adhesion()
|
||||
)
|
||||
_("The cotisation of %(member_name)s has been successfully \
|
||||
extended to %(end_date)s.") % {
|
||||
member_name: user.pseudo,
|
||||
end_date: user.end_adhesion()
|
||||
}
|
||||
)
|
||||
# Else, only tell the invoice was created
|
||||
else:
|
||||
messages.success(request, "La facture a été crée")
|
||||
messages.success(
|
||||
request,
|
||||
_("The invoice has been successuflly created.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': userid}
|
||||
))
|
||||
messages.error(
|
||||
request,
|
||||
u"Il faut au moins un article valide pour créer une facture"
|
||||
_("You need to choose at least one article.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
|
@ -601,31 +779,37 @@ def new_facture_solde(request, userid):
|
|||
}, 'cotisations/new_facture_solde.html', request)
|
||||
|
||||
|
||||
# TODO : change recharge to refill
|
||||
@login_required
|
||||
def recharge(request):
|
||||
"""
|
||||
View used to refill the balance by using online payment.
|
||||
"""
|
||||
if AssoOption.get_cached_value('payment') == 'NONE':
|
||||
messages.error(
|
||||
request,
|
||||
"Le paiement en ligne est désactivé."
|
||||
_("Online payment is disabled.")
|
||||
)
|
||||
return redirect(reverse(
|
||||
'users:profil',
|
||||
kwargs={'userid': request.user.id}
|
||||
))
|
||||
f = RechargeForm(request.POST or None, user=request.user)
|
||||
if f.is_valid():
|
||||
facture = Facture(user=request.user)
|
||||
paiement, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne')
|
||||
facture.paiement = paiement
|
||||
refill_form = RechargeForm(request.POST or None, user=request.user)
|
||||
if refill_form.is_valid():
|
||||
invoice = Facture(user=request.user)
|
||||
payment, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne')
|
||||
facture.paiement = payment
|
||||
facture.valid = False
|
||||
facture.save()
|
||||
v = Vente.objects.create(
|
||||
facture=facture,
|
||||
purchase = Vente.objects.create(
|
||||
facture=invoice,
|
||||
name='solde',
|
||||
prix=f.cleaned_data['value'],
|
||||
number=1,
|
||||
prix=refill_form.cleaned_data['value'],
|
||||
number=1
|
||||
)
|
||||
v.save()
|
||||
content = payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](facture, request)
|
||||
purchase.save()
|
||||
content = online_payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](invoice, request)
|
||||
return render(request, 'cotisations/payment.html', content)
|
||||
return form({'rechargeform':f}, 'cotisations/recharge.html', request)
|
||||
return form({
|
||||
'rechargeform': refill_form
|
||||
}, 'cotisations/recharge.html', request)
|
||||
|
|
|
@ -82,6 +82,7 @@ INSTALLED_APPS = (
|
|||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
|
@ -120,7 +121,7 @@ WSGI_APPLICATION = 're2o.wsgi.application'
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
|
||||
|
|
Loading…
Reference in a new issue