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

Merge branch 'translation' into 'master'

Translation of cotisation app

See merge request federez/re2o!114
This commit is contained in:
chirac 2018-04-09 20:13:31 +02:00
commit a38e56cde4
26 changed files with 2052 additions and 680 deletions

View file

@ -25,6 +25,7 @@
Here are defined some functions to check acl on the application. Here are defined some functions to check acl on the application.
""" """
from django.utils.translation import ugettext as _
def can_view(user): def can_view(user):
"""Check if an user can view the application. """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). viewing is granted and msg is a message (can be None).
""" """
can = user.has_module_perms('cotisations') 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.")

View file

@ -20,19 +20,18 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" """
Forms de l'application cotisation de re2o. Dépendance avec les models, Forms for the 'cotisation' app of re2o. It highly depends on
importé par les views. :cotisations:models and is mainly used by :cotisations:views.
Permet de créer une nouvelle facture pour un user (NewFactureForm), The following forms are mainly used to create, edit or delete
et de l'editer (soit l'user avec EditFactureForm, anything related to 'cotisations' :
soit le trésorier avec TrezEdit qui a plus de possibilités que self * Payments Methods
notamment sur le controle trésorier SelectArticleForm est utilisée * Banks
lors de la creation d'une facture en * Invoices
parrallèle de NewFacture pour le choix des articles désirés. * Articles
(la vue correspondante est unique)
ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter, See the details for each of these operations in the documentation
éditer ou supprimer une banque/moyen de paiement ou un article of each of the method.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
@ -40,6 +39,9 @@ from django import forms
from django.db.models import Q from django.db.models import Q
from django.forms import ModelForm, Form from django.forms import ModelForm, Form
from django.core.validators import MinValueValidator,MaxValueValidator 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 .models import Article, Paiement, Facture, Banque
from preferences.models import OptionalUser from preferences.models import OptionalUser
from users.models import User from users.models import User
@ -48,17 +50,21 @@ from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
class NewFactureForm(FormRevMixin, ModelForm): 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): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) super(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['cheque'].required = False
self.fields['banque'].required = False self.fields['banque'].required = False
self.fields['cheque'].label = 'Numero de chèque' self.fields['cheque'].label = _("Cheque number")
self.fields['banque'].empty_label = "Non renseigné" self.fields['banque'].empty_label = _("Not specified")
self.fields['paiement'].empty_label = "Séléctionner\ self.fields['paiement'].empty_label = \
un moyen de paiement" _("Select a payment method")
paiement_list = Paiement.objects.filter(type_paiement=1) paiement_list = Paiement.objects.filter(type_paiement=1)
if paiement_list: if paiement_list:
self.fields['paiement'].widget\ self.fields['paiement'].widget\
@ -70,96 +76,119 @@ class NewFactureForm(FormRevMixin, ModelForm):
def clean(self): def clean(self):
cleaned_data = super(NewFactureForm, self).clean() cleaned_data = super(NewFactureForm, self).clean()
paiement = cleaned_data.get("paiement") paiement = cleaned_data.get('paiement')
cheque = cleaned_data.get("cheque") cheque = cleaned_data.get('cheque')
banque = cleaned_data.get("banque") banque = cleaned_data.get('banque')
if not paiement: if not paiement:
raise forms.ValidationError("Le moyen de paiement est obligatoire") raise forms.ValidationError(
elif paiement.type_paiement == "check" and not (cheque and banque): _("A payment method must be specified.")
raise forms.ValidationError("Le numéro de chèque et\ )
la banque sont obligatoires.") elif paiement.type_paiement == 'check' and not (cheque and banque):
raise forms.ValidationError(
_("A cheque number and a bank must be specified.")
)
return cleaned_data return cleaned_data
class CreditSoldeForm(NewFactureForm): class 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): class Meta(NewFactureForm.Meta):
model = Facture model = Facture
fields = ['paiement', 'banque', 'cheque'] fields = ['paiement', 'banque', 'cheque']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CreditSoldeForm, self).__init__(*args, **kwargs) super(CreditSoldeForm, self).__init__(*args, **kwargs)
# TODO : change solde to balance
self.fields['paiement'].queryset = Paiement.objects.exclude( self.fields['paiement'].queryset = Paiement.objects.exclude(
moyen='solde' moyen='solde'
).exclude(moyen="Solde") ).exclude(moyen='Solde')
montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True)
class SelectUserArticleForm(FormRevMixin, Form): 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( article = forms.ModelChoiceField(
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')), queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')),
label="Article", label=_l("Article"),
required=True required=True
) )
quantity = forms.IntegerField( quantity = forms.IntegerField(
label="Quantité", label=_l("Quantity"),
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
required=True required=True
) )
class SelectClubArticleForm(Form): 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( article = forms.ModelChoiceField(
queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')), queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')),
label="Article", label=_l("Article"),
required=True required=True
) )
quantity = forms.IntegerField( quantity = forms.IntegerField(
label="Quantité", label=_l("Quantity"),
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
required=True required=True
) )
# TODO : change Facture to Invoice
class NewFactureFormPdf(Form): class NewFactureFormPdf(Form):
"""Creation d'un pdf facture par le trésorier""" """
Form used to create a custom PDF invoice.
"""
article = forms.ModelMultipleChoiceField( article = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(), queryset=Article.objects.all(),
label="Article" label=_l("Article")
) )
number = forms.IntegerField( number = forms.IntegerField(
label="Quantité", label=_l("Quantity"),
validators=[MinValueValidator(1)] validators=[MinValueValidator(1)]
) )
paid = forms.BooleanField(label="Payé", required=False) paid = forms.BooleanField(label=_l("Paid"), required=False)
dest = forms.CharField(required=True, max_length=255, label="Destinataire") # TODO : change dest field to recipient
chambre = forms.CharField(required=False, max_length=10, label="Adresse") 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( fid = forms.CharField(
required=True, required=True,
max_length=10, max_length=10,
label="Numéro de la facture" label=_l("Invoice number")
) )
# TODO : change Facture to Invoice
class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): 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): class Meta(NewFactureForm.Meta):
# TODO : change Facture to Invoice
model = Facture model = Facture
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# TODO : change Facture to Invoice
super(EditFactureForm, self).__init__(*args, **kwargs) super(EditFactureForm, self).__init__(*args, **kwargs)
self.fields['user'].label = 'Adherent' self.fields['user'].label = _("Member")
self.fields['user'].empty_label = "Séléctionner\ self.fields['user'].empty_label = \
l'adhérent propriétaire" _("Select the proprietary member")
self.fields['valid'].label = 'Validité de la facture' self.fields['valid'].label = _("Validated invoice")
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):
"""Creation d'un article. Champs : nom, cotisation, durée""" """
Form used to create an article.
"""
class Meta: class Meta:
model = Article model = Article
fields = '__all__' fields = '__all__'
@ -167,15 +196,17 @@ class ArticleForm(FormRevMixin, ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs) 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): 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( articles = forms.ModelMultipleChoiceField(
queryset=Article.objects.none(), queryset=Article.objects.none(),
label="Articles actuels", label=_l("Existing articles"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -188,26 +219,39 @@ class DelArticleForm(FormRevMixin, Form):
self.fields['articles'].queryset = Article.objects.all() self.fields['articles'].queryset = Article.objects.all()
# TODO : change Paiement to Payment
class PaiementForm(FormRevMixin, ModelForm): 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: class Meta:
model = Paiement model = Paiement
# TODO : change moyen to method and type_paiement to payment_type
fields = ['moyen', 'type_paiement'] fields = ['moyen', 'type_paiement']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs) super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['moyen'].label = 'Moyen de paiement à ajouter' self.fields['moyen'].label = _("Payment method name")
self.fields['type_paiement'].label = 'Type de paiement à ajouter' 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): 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( paiements = forms.ModelMultipleChoiceField(
queryset=Paiement.objects.none(), queryset=Paiement.objects.none(),
label="Moyens de paiement actuels", label=_l("Existing payment method"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -220,23 +264,32 @@ class DelPaiementForm(FormRevMixin, Form):
self.fields['paiements'].queryset = Paiement.objects.all() self.fields['paiements'].queryset = Paiement.objects.all()
# TODO : change banque to bank
class BanqueForm(FormRevMixin, ModelForm): class BanqueForm(FormRevMixin, ModelForm):
"""Creation d'une banque, field name""" """
Form used to create a bank.
"""
class Meta: class Meta:
# TODO : change banque to bank
model = Banque model = Banque
fields = ['name'] fields = ['name']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs) 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): 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( banques = forms.ModelMultipleChoiceField(
queryset=Banque.objects.none(), queryset=Banque.objects.none(),
label="Banques actuelles", label=_l("Existing banks"),
widget=forms.CheckboxSelectMultiple widget=forms.CheckboxSelectMultiple
) )
@ -249,43 +302,59 @@ class DelBanqueForm(FormRevMixin, Form):
self.fields['banques'].queryset = Banque.objects.all() self.fields['banques'].queryset = Banque.objects.all()
# TODO : change facture to Invoice
class NewFactureSoldeForm(NewFactureForm): 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): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
self.fields['cheque'].required = False self.fields['cheque'].required = False
self.fields['banque'].required = False self.fields['banque'].required = False
self.fields['cheque'].label = 'Numero de chèque' self.fields['cheque'].label = _('Cheque number')
self.fields['banque'].empty_label = "Non renseigné" self.fields['banque'].empty_label = _("Not specified")
self.fields['paiement'].empty_label = "Séléctionner\ self.fields['paiement'].empty_label = \
une bite de paiement" _("Select a payment method")
# TODO : change paiement to payment
paiement_list = Paiement.objects.filter(type_paiement=1) paiement_list = Paiement.objects.filter(type_paiement=1)
if paiement_list: if paiement_list:
self.fields['paiement'].widget\ self.fields['paiement'].widget\
.attrs['data-cheque'] = paiement_list.first().id .attrs['data-cheque'] = paiement_list.first().id
class Meta: class Meta:
# TODO : change facture to invoice
model = Facture model = Facture
# TODO : change paiement to payment and baque to bank
fields = ['paiement', 'banque'] fields = ['paiement', 'banque']
def clean(self): def clean(self):
cleaned_data = super(NewFactureSoldeForm, self).clean() cleaned_data = super(NewFactureSoldeForm, self).clean()
# TODO : change paiement to payment
paiement = cleaned_data.get("paiement") paiement = cleaned_data.get("paiement")
cheque = cleaned_data.get("cheque") cheque = cleaned_data.get("cheque")
# TODO : change banque to bank
banque = cleaned_data.get("banque") banque = cleaned_data.get("banque")
# TODO : change paiement to payment
if not paiement: 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): elif paiement.type_paiement == "check" and not (cheque and banque):
raise forms.ValidationError("Le numéro de chèque et\ raise forms.ValidationError(
la banque sont obligatoires.") _("A cheque number and a bank must be specified.")
)
return cleaned_data return cleaned_data
# TODO : Better name and docstring
class RechargeForm(FormRevMixin, Form): class RechargeForm(FormRevMixin, Form):
"""
Form used to refill a user's balance
"""
value = forms.FloatField( value = forms.FloatField(
label='Valeur', label=_l("Amount"),
min_value=0.01, min_value=0.01,
validators = [] validators = []
) )
@ -297,7 +366,21 @@ class RechargeForm(FormRevMixin, Form):
def clean_value(self): def clean_value(self):
value = self.cleaned_data['value'] value = self.cleaned_data['value']
if value < OptionalUser.get_cached_value('min_online_payment'): 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'): 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 return value

Binary file not shown.

View 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é."

View file

@ -21,26 +21,13 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" """
Definition des models bdd pour les factures et cotisation. The database models for the 'cotisation' app of re2o.
Pièce maitresse : l'ensemble du code intelligent se trouve ici, The goal is to keep the main actions here, i.e. the 'clean' and 'save'
dans les clean et save des models ainsi que de leur methodes supplémentaires. 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), For further details on each of those models, see the documentation details for
une ou plusieurs ventes each.
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
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
@ -55,55 +42,101 @@ from django.core.validators import MinValueValidator
from django.db.models import Max from django.db.models import Max
from django.utils import timezone from django.utils import timezone
from machines.models import regen 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.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin from re2o.mixins import AclMixin, RevMixin
# TODO : change facture to invoice
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): 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 The model for an invoice. It reprensents the fact that a user paid for
et si il y a lieu un numero pour les chèques. Possède les valeurs something (it can be multiple article paid at once).
valides et controle (trésorerie)"""
PRETTY_NAME = "Factures émises" 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) user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey( banque = models.ForeignKey(
'Banque', 'Banque',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True, blank=True,
null=True) null=True
cheque = models.CharField(max_length=255, blank=True) )
date = models.DateTimeField(auto_now_add=True) # TODO : maybe change to cheque nummber because not evident
valid = models.BooleanField(default=True) cheque = models.CharField(
control = models.BooleanField(default=False) 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: class Meta:
abstract = False abstract = False
permissions = ( permissions = (
("change_facture_control", "Peut changer l'etat de controle"), # TODO : change facture to invoice
("change_facture_pdf", "Peut éditer une facture pdf"), ('change_facture_control', _l("Can change the \"controlled\" state")),
("view_facture", "Peut voir un objet facture"), # TODO : seems more likely to be call create_facture_pdf or create_invoice_pdf
("change_all_facture", "Superdroit, peut modifier toutes les factures"), ('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): def linked_objects(self):
"""Return linked objects : machine and domain. """Return linked objects : machine and domain.
Usefull in history display""" Usefull in history display"""
return self.vente_set.all() return self.vente_set.all()
# TODO : change prix to price
def prix(self): def prix(self):
"""Renvoie le prix brut sans les quantités. Méthode """
dépréciée""" Returns: the raw price without the quantities.
prix = Vente.objects.filter( Deprecated, use :total_price instead.
"""
price = Vente.objects.filter(
facture=self facture=self
).aggregate(models.Sum('prix'))['prix__sum'] ).aggregate(models.Sum('prix'))['prix__sum']
return prix return price
# TODO : change prix to price
def prix_total(self): 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( return Vente.objects.filter(
facture=self facture=self
).aggregate( ).aggregate(
@ -114,7 +147,10 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
)['total'] )['total']
def name(self): 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( name = ' - '.join(Vente.objects.filter(
facture=self facture=self
).values_list('name', flat=True)) ).values_list('name', flat=True))
@ -122,44 +158,41 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
def can_edit(self, user_request, *args, **kwargs): def can_edit(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.change_facture'): 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]: 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\ elif not user_request.has_perm('cotisations.change_all_facture') and\
(self.control or not self.valid): (self.control or not self.valid):
return False, u"Vous n'avez pas le droit d'éditer une facture\ return False, _("You don't have the right to edit an invoice already controlled or invalidated.")
controlée ou invalidée par un trésorier"
else: else:
return True, None return True, None
def can_delete(self, user_request, *args, **kwargs): def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_facture'): if not user_request.has_perm('cotisations.delete_facture'):
return False, 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]: 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: if self.control or not self.valid:
return False, u"Vous ne pouvez pas supprimer une facture\ return False, _("You don't have the right to delete an invoice already controlled or invalidated.")
contrôlée ou invalidée par un trésorier"
else: else:
return True, None return True, None
def can_view(self, user_request, *args, **kwargs): def can_view(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.view_facture') and\ if not user_request.has_perm('cotisations.view_facture') and\
self.user != user_request: self.user != user_request:
return False, u"Vous ne pouvez pas afficher l'historique d'une\ return False, _("You don't have the right to see someone else's invoices history.")
facture d'un autre user que vous sans droit cableur"
elif not self.valid: 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: else:
return True, None return True, None
@staticmethod @staticmethod
def can_change_control(user_request, *args, **kwargs): 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 @staticmethod
def can_change_pdf(user_request, *args, **kwargs): 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): def __init__(self, *args, **kwargs):
super(Facture, self).__init__(*args, **kwargs) super(Facture, self).__init__(*args, **kwargs)
@ -173,7 +206,9 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
@receiver(post_save, sender=Facture) @receiver(post_save, sender=Facture)
def facture_post_save(sender, **kwargs): 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'] facture = kwargs['instance']
user = facture.user user = facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) 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) @receiver(post_delete, sender=Facture)
def facture_post_delete(sender, **kwargs): 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 = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
# TODO : change Vente to Purchase
class Vente(RevMixin, AclMixin, models.Model): 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 The model defining a purchase. It consist of one type of article being
iscotisation""" sold. In particular there may be multiple purchases in a single invoice.
PRETTY_NAME = "Ventes effectuées"
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 = ( COTISATION_TYPE = (
('Connexion', 'Connexion'), ('Connexion', _l("Connexion")),
('Adhesion', 'Adhesion'), ('Adhesion', _l("Membership")),
('All', 'All'), ('All', _l("Both of them")),
) )
facture = models.ForeignKey('Facture', on_delete=models.CASCADE) # TODO : change facture to invoice
number = models.IntegerField(validators=[MinValueValidator(1)]) facture = models.ForeignKey(
name = models.CharField(max_length=255) 'Facture',
prix = models.DecimalField(max_digits=5, decimal_places=2) 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( duration = models.PositiveIntegerField(
help_text="Durée exprimée en mois entiers",
blank=True, 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( type_cotisation = models.CharField(
choices=COTISATION_TYPE, choices=COTISATION_TYPE,
blank=True, blank=True,
null=True, null=True,
max_length=255 max_length=255,
verbose_name=_l("Type of cotisation")
) )
class Meta: class Meta:
permissions = ( permissions = (
("view_vente", "Peut voir un objet vente"), ('view_vente', _l("Can see a purchase's details")),
("change_all_vente", "Superdroit, peut modifier toutes les ventes"), ('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): 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 return self.prix*self.number
def update_cotisation(self): 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 Update the related object 'cotisation' if there is one. Based on the
la vente""" duration of the purchase.
"""
if hasattr(self, 'cotisation'): if hasattr(self, 'cotisation'):
cotisation = self.cotisation cotisation = self.cotisation
cotisation.date_end = cotisation.date_start + relativedelta( cotisation.date_end = cotisation.date_start + relativedelta(
@ -234,9 +309,11 @@ class Vente(RevMixin, AclMixin, models.Model):
return return
def create_cotis(self, date_start=False): 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 Update and create a 'cotisation' related object if there is a
la durée""" cotisation_type defined (which means the article sold represents
a cotisation)
"""
if not hasattr(self, 'cotisation') and self.type_cotisation: if not hasattr(self, 'cotisation') and self.type_cotisation:
cotisation = Cotisation(vente=self) cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation cotisation.type_cotisation = self.type_cotisation
@ -264,41 +341,44 @@ class Vente(RevMixin, AclMixin, models.Model):
return return
def save(self, *args, **kwargs): 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: if self.type_cotisation and not self.duration:
raise ValidationError("Cotisation et durée doivent être présents\ raise ValidationError(
ensembles") _("A cotisation should always have a duration.")
)
self.update_cotisation() self.update_cotisation()
super(Vente, self).save(*args, **kwargs) super(Vente, self).save(*args, **kwargs)
def can_edit(self, user_request, *args, **kwargs): def can_edit(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.change_vente'): 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]: 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\ elif not user_request.has_perm('cotisations.change_all_vente') and\
(self.facture.control or not self.facture.valid): (self.facture.control or not self.facture.valid):
return False, u"Vous n'avez pas le droit d'éditer une vente\ return False, _("You don't have the right to edit a purchase already controlled or invalidated.")
controlée ou invalidée par un trésorier"
else: else:
return True, None return True, None
def can_delete(self, user_request, *args, **kwargs): def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_vente'): 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]: 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: if self.facture.control or not self.facture.valid:
return False, u"Vous ne pouvez pas supprimer une vente\ return False, _("You don't have the right to delete a purchase already controlled or invalidated.")
contrôlée ou invalidée par un trésorier"
else: else:
return True, None return True, None
def can_view(self, user_request, *args, **kwargs): def can_view(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.view_vente') and\ if not user_request.has_perm('cotisations.view_vente') and\
self.facture.user != user_request: self.facture.user != user_request:
return False, u"Vous ne pouvez pas afficher l'historique d'une\ return False, _("You don't have the right to see someone else's purchase history.")
facture d'un autre user que vous sans droit cableur"
else: else:
return True, None return True, None
@ -306,81 +386,109 @@ class Vente(RevMixin, AclMixin, models.Model):
return str(self.name) + ' ' + str(self.facture) return str(self.name) + ' ' + str(self.facture)
# TODO : change vente to purchase
@receiver(post_save, sender=Vente) @receiver(post_save, sender=Vente)
def vente_post_save(sender, **kwargs): 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) """ Creates a 'cotisation' related object if needed and synchronise the
vente = kwargs['instance'] LDAP user when a purchase has been saved.
"""
purchase = kwargs['instance']
if hasattr(vente, 'cotisation'): if hasattr(vente, 'cotisation'):
vente.cotisation.vente = vente purchase.cotisation.vente = purchase
vente.cotisation.save() purchase.cotisation.save()
if vente.type_cotisation: if purchase.type_cotisation:
vente.create_cotis() purchase.create_cotis()
vente.cotisation.save() purchase.cotisation.save()
user = vente.facture.user user = purchase.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
# TODO : change vente to purchase
@receiver(post_delete, sender=Vente) @receiver(post_delete, sender=Vente)
def vente_post_delete(sender, **kwargs): def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, on synchronise l'user ldap (ex """
suppression d'une cotisation""" Synchronise the LDAP user after a purchase has been deleted.
vente = kwargs['instance'] """
if vente.type_cotisation: purchase = kwargs['instance']
user = vente.facture.user if purchase.type_cotisation:
user = purchase.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Article(RevMixin, AclMixin, models.Model): class Article(RevMixin, AclMixin, models.Model):
"""Liste des articles en vente : prix, nom, et attribut iscotisation """
et duree si c'est une cotisation""" The definition of an article model. It represents an type of object that can be sold to the user.
PRETTY_NAME = "Articles en vente"
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 = ( USER_TYPES = (
('Adherent', 'Adherent'), ('Adherent', _l("Member")),
('Club', 'Club'), ('Club', _l("Club")),
('All', 'All'), ('All', _l("Both of them")),
) )
COTISATION_TYPE = ( COTISATION_TYPE = (
('Connexion', 'Connexion'), ('Connexion', _l("Connexion")),
('Adhesion', 'Adhesion'), ('Adhesion', _l("Membership")),
('All', 'All'), ('All', _l("Both of them")),
) )
name = models.CharField(max_length=255) name = models.CharField(
prix = models.DecimalField(max_digits=5, decimal_places=2) 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( duration = models.PositiveIntegerField(
help_text="Durée exprimée en mois entiers",
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)]) validators=[MinValueValidator(0)],
verbose_name=_l("Duration (in whole month)")
)
type_user = models.CharField( type_user = models.CharField(
choices=USER_TYPES, choices=USER_TYPES,
default='All', default='All',
max_length=255 max_length=255,
verbose_name=_l("Type of users concerned")
) )
type_cotisation = models.CharField( type_cotisation = models.CharField(
choices=COTISATION_TYPE, choices=COTISATION_TYPE,
default=None, default=None,
blank=True, blank=True,
null=True, null=True,
max_length=255 max_length=255,
verbose_name=_l("Type of cotisation")
) )
unique_together = ('name', 'type_user') unique_together = ('name', 'type_user')
class Meta: class Meta:
permissions = ( 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): def clean(self):
if self.name.lower() == "solde": if self.name.lower() == 'solde':
raise ValidationError("Solde est un nom d'article invalide") raise ValidationError(
_("Solde is a reserved article name")
)
if self.type_cotisation and not self.duration: if self.type_cotisation and not self.duration:
raise ValidationError( raise ValidationError(
"La durée est obligatoire si il s'agit d'une cotisation" _("Duration must be specified for a cotisation")
) )
def __str__(self): def __str__(self):
@ -388,99 +496,150 @@ class Article(RevMixin, AclMixin, models.Model):
class Banque(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: class Meta:
permissions = ( 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): def __str__(self):
return self.name return self.name
# TODO : change Paiement to Payment
class Paiement(RevMixin, AclMixin, models.Model): 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 = ( PAYMENT_TYPES = (
(0, 'Autre'), (0, _l("Standard")),
(1, 'Chèque'), (1, _l("Cheque")),
) )
moyen = models.CharField(max_length=255) # TODO : change moyen to method
type_paiement = models.IntegerField(choices=PAYMENT_TYPES, default=0) 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: class Meta:
permissions = ( 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): def __str__(self):
return self.moyen return self.moyen
def clean(self): def clean(self):
"""
Override of the herited clean function to get a correct name
"""
self.moyen = self.moyen.title() self.moyen = self.moyen.title()
def save(self, *args, **kwargs): 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: if Paiement.objects.filter(type_paiement=1).count() > 1:
raise ValidationError("On ne peut avoir plusieurs mode de paiement\ raise ValidationError(
chèque") _("You cannot have multiple payment method of type cheque")
)
super(Paiement, self).save(*args, **kwargs) super(Paiement, self).save(*args, **kwargs)
class Cotisation(RevMixin, AclMixin, models.Model): 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 = ( COTISATION_TYPE = (
('Connexion', 'Connexion'), ('Connexion', _l("Connexion")),
('Adhesion', 'Adhesion'), ('Adhesion', _l("Membership")),
('All', 'All'), ('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( type_cotisation = models.CharField(
choices=COTISATION_TYPE, choices=COTISATION_TYPE,
max_length=255, max_length=255,
default='All', 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: class Meta:
permissions = ( permissions = (
("view_cotisation", "Peut voir un objet cotisation"), ('view_cotisation', _l("Can see a cotisation's details")),
("change_all_cotisation", "Superdroit, peut modifier toutes les cotisations"), ('change_all_cotisation', _l("Can edit the previous cotisations")),
) )
def can_edit(self, user_request, *args, **kwargs): def can_edit(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.change_cotisation'): 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\ elif not user_request.has_perm('cotisations.change_all_cotisation') and\
(self.vente.facture.control or not self.vente.facture.valid): (self.vente.facture.control or not self.vente.facture.valid):
return False, u"Vous n'avez pas le droit d'éditer une cotisation\ return False, _("You don't have the right to edit a cotisation already controlled or invalidated.")
controlée ou invalidée par un trésorier"
else: else:
return True, None return True, None
def can_delete(self, user_request, *args, **kwargs): def can_delete(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.delete_cotisation'): 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: if self.vente.facture.control or not self.vente.facture.valid:
return False, u"Vous ne pouvez pas supprimer une cotisations\ return False, _("You don't have the right to delete a cotisation already controlled or invalidated.")
contrôlée ou invalidée par un trésorier"
else: else:
return True, None return True, None
def can_view(self, user_request, *args, **kwargs): def can_view(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.view_cotisation') and\ if not user_request.has_perm('cotisations.view_cotisation') and\
self.vente.facture.user != user_request: self.vente.facture.user != user_request:
return False, u"Vous ne pouvez pas afficher l'historique d'une\ return False, _("You don't have the right to see someone else's cotisation history.")
cotisation d'un autre user que vous sans droit cableur"
else: else:
return True, None return True, None
@ -490,16 +649,23 @@ class Cotisation(RevMixin, AclMixin, models.Model):
@receiver(post_save, sender=Cotisation) @receiver(post_save, sender=Cotisation)
def cotisation_post_save(sender, **kwargs): 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('dns')
regen('dhcp') regen('dhcp')
regen('mac_ip_list') regen('mac_ip_list')
regen('mailing') regen('mailing')
# TODO : should be name cotisation_post_delete
@receiver(post_delete, sender=Cotisation) @receiver(post_delete, sender=Cotisation)
def vente_post_delete(sender, **kwargs): 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'] cotisation = kwargs['instance']
regen('mac_ip_list') regen('mac_ip_list')
regen('mailing') regen('mailing')

View file

@ -8,6 +8,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.datastructures import MultiValueDictKeyError from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from collections import OrderedDict from collections import OrderedDict
@ -19,10 +20,15 @@ from .payment_utils.comnpay import Payment as ComnpayPayment
@csrf_exempt @csrf_exempt
@login_required @login_required
def accept_payment(request, factureid): def accept_payment(request, factureid):
"""
The view called when an online payment has been accepted.
"""
facture = get_object_or_404(Facture, id=factureid) facture = get_object_or_404(Facture, id=factureid)
messages.success( messages.success(
request, 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})) return redirect(reverse('users:profil', kwargs={'userid':request.user.id}))
@ -30,14 +36,22 @@ def accept_payment(request, factureid):
@csrf_exempt @csrf_exempt
@login_required @login_required
def refuse_payment(request): def refuse_payment(request):
"""
The view called when an online payment has been refused.
"""
messages.error( messages.error(
request, request,
"Le paiement a été refusé." _("The payment has been refused.")
) )
return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) return redirect(reverse('users:profil', kwargs={'userid':request.user.id}))
@csrf_exempt @csrf_exempt
def ipn(request): 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() p = ComnpayPayment()
order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', ) order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', )
try: try:
@ -52,7 +66,7 @@ def ipn(request):
idTpe = request.POST['idTpe'] idTpe = request.POST['idTpe']
idTransaction = request.POST['idTransaction'] 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'): if not idTpe == AssoOption.get_cached_value('payment_id'):
return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") return HttpResponseBadRequest("HTTP/1.1 400 Bad Request")
@ -63,22 +77,28 @@ def ipn(request):
facture = get_object_or_404(Facture, id=factureid) 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: 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() facture.delete()
# And send the response to Comnpay indicating we have well
# On notifie au serveur ComNPay qu'on a reçu les données pour traitement # received the failure information.
return HttpResponse("HTTP/1.1 200 OK") return HttpResponse("HTTP/1.1 200 OK")
facture.valid = True facture.valid = True
facture.save() 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") return HttpResponse("HTTP/1.0 200 OK")
def comnpay(facture, request): 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() host = request.get_host()
p = ComnpayPayment( p = ComnpayPayment(
str(AssoOption.get_cached_value('payment_id')), str(AssoOption.get_cached_value('payment_id')),
@ -105,6 +125,7 @@ def comnpay(facture, request):
return r return r
# The payment systems supported by re2o
PAYMENT_SYSTEM = { PAYMENT_SYSTEM = {
'COMNPAY' : comnpay, 'COMNPAY' : comnpay,
'NONE' : None 'NONE' : None

View file

@ -23,15 +23,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Article</th> <th>{% trans "Article" %}</th>
<th>Prix</th> <th>{% trans "Price" %}</th>
<th>Type Cotisation</th> <th>{% trans "Cotisation type" %}</th>
<th>Durée (mois)</th> <th>{% trans "Duration (month)" %}</th>
<th>Article pour</th> <th>{% trans "Concerned users" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -44,11 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ article.type_user }}</td> <td>{{ article.type_user }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit article %} {% can_edit article %}
<a class="btn btn-primary btn-sm" role="button" title="É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> <i class="fa fa-edit"></i>
</a> </a>
{% acl_end %} {% 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> <i class="fa fa-history"></i>
</a> </a>
</td> </td>

View file

@ -23,11 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Banque</th> <th>{% trans "Bank" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -36,11 +37,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ banque.name }}</td> <td>{{ banque.name }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit banque %} {% 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> <i class="fa fa-edit"></i>
</a> </a>
{% acl_end %} {% 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> <i class="fa fa-history"></i>
</a> </a>
</td> </td>

View file

@ -23,20 +23,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
<div class="table-responsive"> <div class="table-responsive">
{% if facture_list.paginator %} {% if facture_list.paginator %}
{% include "pagination.html" with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% include "buttons/sort.html" with prefix='cotis' col='user' text='Utilisateur' %}</th> <th>
<th>Designation</th> {% trans "User" as tr_user %}
<th>Prix total</th> {% include 'buttons/sort.html' with prefix='cotis' col='user' text=tr_user %}
<th>{% include "buttons/sort.html" with prefix='cotis' col='paiement' text='Moyen de paiement' %}</th> </th>
<th>{% include "buttons/sort.html" with prefix='cotis' col='date' text='Date' %}</th> <th>{% trans "Designation" %}</th>
<th>{% include "buttons/sort.html" with prefix='cotis' col='id' text='Id facture' %}</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>
<th></th> <th></th>
</tr> </tr>
@ -50,39 +64,49 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ facture.date }}</td> <td>{{ facture.date }}</td>
<td>{{ facture.id }}</td> <td>{{ facture.id }}</td>
<td> <td>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="editionfacture" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-default dropdown-toggle" type="button" id="editinvoice" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Edition {% trans "Edit" %}<span class="caret"></span>
<span class="caret"></span> </button>
</button> <ul class="dropdown-menu" aria-labelledby="editinvoice">
<ul class="dropdown-menu" aria-labelledby="editionfacture"> {% can_edit facture %}
{% can_edit facture %} <li>
<li><a href="{% url 'cotisations:edit-facture' facture.id %}"><i class="fa fa-dollar-sign"></i> Modifier</a></li> <a href="{% url 'cotisations:edit-facture' facture.id %}">
{% acl_else %} <i class="fa fa-dollar-sign"></i> {% trans "Edit" %}
<li>Facture controlée</li> </a>
{% acl_end %} </li>
{% can_delete facture %} {% acl_else %}
<li><a href="{% url 'cotisations:del-facture' facture.id %}"><i class="fa fa-trash"></i> Supprimer</a></li> <li>{% trans "Controlled invoice" %}</li>
{% acl_end %} {% acl_end %}
<li><a href="{% url 'cotisations:history' 'facture' facture.id %}"><i class="fa fa-history"></i> Historique</a></li> {% can_delete facture %}
</ul> <li>
</div> <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> {% trans "Historique" %}
</a>
</li>
</ul>
</div>
</td> </td>
<td> <td>
{% if facture.valid %} {% if facture.valid %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:facture-pdf' facture.id %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:facture-pdf' facture.id %}">
<i class="fa fa-file-pdf"></i> <i class="fa fa-file-pdf"></i> {% trans "PDF" %}
PDF
</a> </a>
{% else %} {% else %}
<i class="text-danger">Facture invalide</i> <i class="text-danger">{% trans "Invalidated invoice" %}</i>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% if facture_list.paginator %} {% if facture_list.paginator %}
{% include "pagination.html" with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}
</div> </div>

View file

@ -23,11 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Moyen de paiement</th> <th>{% trans "Payment method" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -35,12 +36,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ paiement.moyen }}</td> <td>{{ paiement.moyen }}</td>
<td class="text-right"> <td class="text-right">
{% can_edit paiement %} {% can_edit paiement %}
<a class="btn btn-primary btn-sm" role="button" title="É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> <i class="fa fa-edit"></i>
</a> </a>
{% acl_end %} {% 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> <i class="fa fa-history"></i>
</a> </a>
</td> </td>

View file

@ -25,13 +25,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load i18n %}
{% block title %}Controle des factures{% endblock %} {% block title %}{% trans "Invoice control" %}{% endblock %}
{% block content %} {% block content %}
<h2>Controle et validité des factures</h2> <h2>{% trans "Invoice control and validation" %}</h2>
{% if facture_list.paginator %} {% if facture_list.paginator %}
{% include "pagination.html" with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
@ -39,24 +40,50 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Profil</th> <th>{% trans "Profil" %}</th>
<th>{% include "buttons/sort.html" with prefix='control' col='name' text='Nom' %}</th> <th>
<th>{% include "buttons/sort.html" with prefix='control' col='surname' text='Prénom' %}</th> {% trans "Last name" as tr_last_name %}
<th>{% include "buttons/sort.html" with prefix='control' col='id' text='Id facture' %}</th> {% include 'buttons/sort.html' with prefix='control' col='name' text=tr_last_name %}
<th>{% include "buttons/sort.html" with prefix='control' col='user-id' text='Id user' %}</th> </th>
<th>Designation</th> <th>
<th>Prix total</th> {% trans "First name" as tr_first_name %}
<th>{% include "buttons/sort.html" with prefix='control' col='paiement' text='Moyen de paiement' %}</th> {% include 'buttons/sort.html' with prefix='control' col='surname' text=tr_first_name %}
<th>{% include "buttons/sort.html" with prefix='control' col='date' text='Date' %}</th> </th>
<th>{% include "buttons/sort.html" with prefix='control' col='valid' text='Valide' %}</th> <th>
<th>{% include "buttons/sort.html" with prefix='control' col='control' text='Contrôlée' %}</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> </tr>
</thead> </thead>
{% for form in controlform.forms %} {% for form in controlform.forms %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<tr> <tr>
<td>
<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> <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>
<td>{{ form.instance.user.name }}</td> <td>{{ form.instance.user.name }}</td>
<td>{{ form.instance.user.surname }}</td> <td>{{ form.instance.user.surname }}</td>
@ -74,10 +101,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% bootstrap_button "Modifier" button_type="submit" icon="star" %} {% trans "Edit" as tr_edit %}
{% bootstrap_button tr_edit button_type='submit' icon='star' %}
</form> </form>
{% endblock %} {% endblock %}
{% if facture_list.paginator %} {% if facture_list.paginator %}
{% include "pagination.html" with list=facture_list %} {% include 'pagination.html' with list=facture_list %}
{% endif %} {% endif %}

View file

@ -24,17 +24,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Création et modification de machines{% endblock %} {% block title %}{% trans "Deletion of cotisations" %}{% endblock %}
{% block content %} {% block content %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h4>Attention, voulez-vous vraiment supprimer cet objet {{ objet_name }} ( {{ objet }} ) ?</h4> <h4>
{% bootstrap_button "Confirmer" button_type="submit" icon="trash" %} {% 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> </form>
<br />
<br />
<br />
{% endblock %} {% endblock %}

View file

@ -26,27 +26,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load massive_bootstrap_form %} {% 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 %} {% block content %}
{% bootstrap_form_errors factureform %} {% bootstrap_form_errors factureform %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h3>Editer la facture</h3> <h3>{% trans "Edit the invoice" %}</h3>
{% massive_bootstrap_form factureform 'user' %} {% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }} {{ venteform.management_form }}
<h3>Articles de la facture</h3> <h3>{% trans "Invoice's articles" %}</h3>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Désignation</th> <th>{% trans "Designation" %}</th>
<th>Quantité</th> <th>{% trans "Quantity" %}</th>
</tr> </tr>
</thead> </thead>
{% for form in venteform.forms %} {% for form in venteform.forms %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<tr> <tr>
<td>{{ form.name }}</td> <td>{{ form.name }}</td>
<td>{{ form.number }}</td> <td>{{ form.number }}</td>
@ -56,7 +57,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</tr> </tr>
{% endfor %} {% endfor %}
</table> </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> </form>
{% endblock %} {% endblock %}

View file

@ -25,8 +25,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load i18n %}
{% block title %}Création et modification de factures{% endblock %} {% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors factureform %} {% 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"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form factureform %} {% bootstrap_form factureform %}
{% bootstrap_button action_name button_type="submit" icon="star" %} {% bootstrap_button action_name button_type='submit' icon='star' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -24,14 +24,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %}
{% block title %}Facture{% endblock %} {% block title %}{% trans "Invoices" %}{% endblock %}
{% block content %} {% block content %}
<h2>Cotisations</h2> <h2>{% trans "Cotisations" %}</h2>
{% include "cotisations/aff_cotisations.html" with facture_list=facture_list %} {% include 'cotisations/aff_cotisations.html' with facture_list=facture_list %}
<br />
<br />
<br />
{% endblock %} {% endblock %}

View file

@ -25,18 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load i18n %}
{% block title %}Articles{% endblock %} {% block title %}{% trans "Articles" %}{% endblock %}
{% block content %} {% block content %}
<h2>Liste des types d'articles</h2> <h2>{% trans "Article types list" %}</h2>
{% can_create Article %} {% 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' %}">
{% acl_end %} <i class="fa fa-cart-plus"></i> {% trans "Add an article type" %}
<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> </a>
{% include "cotisations/aff_article.html" with article_list=article_list %} {% acl_end %}
<br /> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-article' %}">
<br /> <i class="fa fa-trash"></i> {% trans "Delete article types" %}
<br /> </a>
{% include 'cotisations/aff_article.html' with article_list=article_list %}
{% endblock %} {% endblock %}

View file

@ -25,18 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load i18n %}
{% block title %}Banques{% endblock %} {% block title %}{% trans "Banks" %}{% endblock %}
{% block content %} {% block content %}
<h2>Liste des banques</h2> <h2>{% trans "Banks list" %}</h2>
{% can_create Banque %} {% 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' %}">
{% acl_end %} <i class="fa fa-cart-plus"></i> {% trans "Add a bank" %}
<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> </a>
{% include "cotisations/aff_banque.html" with banque_list=banque_list %} {% acl_end %}
<br /> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-banque' %}">
<br /> <i class="fa fa-trash"></i> {% trans "Delete banks" %}
<br /> </a>
{% include 'cotisations/aff_banque.html' with banque_list=banque_list %}
{% endblock %} {% endblock %}

View file

@ -25,18 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load i18n %}
{% block title %}Paiements{% endblock %} {% block title %}{% trans "Payments" %}{% endblock %}
{% block content %} {% block content %}
<h2>Liste des types de paiements</h2> <h2>{% trans "Payment types list" %}</h2>
{% can_create Paiement %} {% 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' %}">
{% acl_end %} <i class="fa fa-cart-plus"></i> {% trans "Add a payment type" %}
<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> </a>
{% include "cotisations/aff_paiement.html" with paiement_list=paiement_list %} {% acl_end %}
<br /> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:del-paiement' %}">
<br /> <i class="fa fa-trash"></i> {% trans "Delete payment types" %}
<br /> </a>
{% include 'cotisations/aff_paiement.html' with paiement_list=paiement_list %}
{% endblock %} {% endblock %}

View file

@ -25,40 +25,45 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load i18n %}
{% block title %}Création et modification de factures{% endblock %} {% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors factureform %} {% bootstrap_form_errors factureform %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h3>Nouvelle facture</h3> <h3>{% trans "New invoice" %}</h3>
<p> <p>
Solde de l'utilisateur : {{ user.solde }} € {% blocktrans %}
User's balance : {{ user.solde }} €
{% endblocktrans %}
</p> </p>
{% bootstrap_form factureform %} {% bootstrap_form factureform %}
{{ venteform.management_form }} {{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select --> <!-- 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"> <div id="form_set" class="form-group">
{% for form in venteform.forms %} {% for form in venteform.forms %}
<div class='product_to_sell form-inline'> <div class='product_to_sell form-inline'>
Article : &nbsp; {% trans "Article" %} : &nbsp;
{% bootstrap_form form label_class='sr-only' %} {% bootstrap_form form label_class='sr-only' %}
&nbsp; &nbsp;
<button class="btn btn-danger btn-sm" <button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
id="id_form-0-article-remove" type="button"> <span class="fa fa-times"></span>
<span class="fa fa-times"></span> </button>
</button>
</div> </div>
{% endfor %} {% endfor %}
</div> </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> <p>
Prix total : <span id="total_price">0,00</span> {% blocktrans %}
Total price : <span id="total_price">0,00</span>
{% endblocktrans %}
</p> </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> </form>
<script type="text/javascript"> <script type="text/javascript">
@ -72,9 +77,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_form venteform.empty_form label_class='sr-only' %} {% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp; &nbsp;
<button class="btn btn-danger btn-sm" <button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button"> id="id_form-__prefix__-article-remove" type="button">
<span class="fa fa-times"></span> <span class="fa fa-times"></span>
</button>` </button>`
function add_article(){ function add_article(){
// Index start at 0 => new_index = number of items // Index start at 0 => new_index = number of items
@ -116,13 +121,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
document.getElementById('id_form-' + i.toString() + '-quantity') document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true); .addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove') document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) { .addEventListener("click", function(event) {
var article = event.target.parentNode; var article = event.target.parentNode;
article.parentNode.removeChild(article); article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --; document.getElementById('id_form-TOTAL_FORMS').value --;
update_price(); update_price();
} }
) )
} }
function set_cheque_info_visibility() { function set_cheque_info_visibility() {
@ -134,9 +139,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
display = 'block'; display = 'block';
} }
document.getElementById("id_Facture-cheque") document.getElementById("id_Facture-cheque")
.parentNode.style.display = display; .parentNode.style.display = display;
document.getElementById("id_Facture-banque") document.getElementById("id_Facture-banque")
.parentNode.style.display = display; .parentNode.style.display = display;
} }
// Add events manager when DOM is fully loaded // Add events manager when DOM is fully loaded

View file

@ -1,4 +1,3 @@
{% extends "cotisations/sidebar.html" %} {% extends "cotisations/sidebar.html" %}
{% comment %} {% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il Re2o est un logiciel d'administration développé initiallement au rezometz. Il
@ -26,36 +25,39 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load i18n %}
{% block title %}Création et modification de factures{% endblock %} {% block title %}{% trans "Invoices creation and edition" %}{% endblock %}
{% block content %} {% block content %}
{% bootstrap_form_errors venteform.management_form %} {% bootstrap_form_errors venteform.management_form %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
<h3>Nouvelle facture</h3> <h3>{% trans "New invoice" %}</h3>
{{ venteform.management_form }} {{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select --> <!-- 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"> <div id="form_set" class="form-group">
{% for form in venteform.forms %} {% for form in venteform.forms %}
<div class='product_to_sell form-inline'> <div class='product_to_sell form-inline'>
Article : &nbsp; {% trans "Article" %} : &nbsp;
{% bootstrap_form form label_class='sr-only' %} {% bootstrap_form form label_class='sr-only' %}
&nbsp; &nbsp;
<button class="btn btn-danger btn-sm" <button class="btn btn-danger btn-sm" id="id_form-0-article-remove" type="button">
id="id_form-0-article-remove" type="button"> <span class="fa fa-times"></span>
<span class="fa fa-times"></span> </button>
</button>
</div> </div>
{% endfor %} {% endfor %}
</div> </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> <p>
Prix total : <span id="total_price">0,00</span> {% blocktrans %}
Total price : <span id="total_price">0,00</span>
{% endblocktrans %}
</p> </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> </form>
<script type="text/javascript"> <script type="text/javascript">
@ -68,10 +70,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
var template = `Article : &nbsp; var template = `Article : &nbsp;
{% bootstrap_form venteform.empty_form label_class='sr-only' %} {% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp; &nbsp;
<button class="btn btn-danger btn-sm" <button class="btn btn-danger btn-sm" id="id_form-__prefix__-article-remove" type="button">
id="id_form-__prefix__-article-remove" type="button"> <span class="fa fa-times"></span>
<span class="fa fa-times"></span> </button>`
</button>`
function add_article(){ function add_article(){
// Index start at 0 => new_index = number of items // Index start at 0 => new_index = number of items
@ -113,13 +114,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
document.getElementById('id_form-' + i.toString() + '-quantity') document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true); .addEventListener("change", update_price, true);
document.getElementById('id_form-' + i.toString() + '-article-remove') document.getElementById('id_form-' + i.toString() + '-article-remove')
.addEventListener("click", function(event) { .addEventListener("click", function(event) {
var article = event.target.parentNode; var article = event.target.parentNode;
article.parentNode.removeChild(article); article.parentNode.removeChild(article);
document.getElementById('id_form-TOTAL_FORMS').value --; document.getElementById('id_form-TOTAL_FORMS').value --;
update_price(); update_price();
} }
) )
} }
function set_cheque_info_visibility() { function set_cheque_info_visibility() {
@ -131,9 +132,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
display = 'block'; display = 'block';
} }
document.getElementById("id_Facture-cheque") document.getElementById("id_Facture-cheque")
.parentNode.style.display = display; .parentNode.style.display = display;
document.getElementById("id_Facture-banque") document.getElementById("id_Facture-banque")
.parentNode.style.display = display; .parentNode.style.display = display;
} }
// Add events manager when DOM is fully loaded // Add events manager when DOM is fully loaded

View file

@ -25,13 +25,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load i18n %}
{% block title %}Rechargement du solde{% endblock %} {% block title %}{% trans "Balance refill" %}{% endblock %}
{% block content %} {% block content %}
<h3>Recharger de {{ amount }} €</h3> <h3>
<form class="form" method="{{ method }}" action="{{action}}"> {% blocktrans %}
{{ content | safe }} Refill of {{ amount }} €
{% bootstrap_button "Payer" button_type="submit" icon="piggy-bank" %} {% endblocktrans %}
</form> </h3>
<form class="form" method="{{ method }}" action="{{ action }}">
{{ content | safe }}
{% trans "Pay" as tr_pay %}
{% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
</form>
{% endblock %} {% endblock %}

View file

@ -25,15 +25,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles%} {% load staticfiles%}
{% load i18n %}
{% block title %}Rechargement du solde{% endblock %} {% block title %}{% trans "Balance refill" %}{% endblock %}
{% block content %} {% block content %}
<h2>Rechargement du solde</h2> <h2>{% trans "Balance refill" %}</h2>
<h3>Solde : <span class="label label-default">{{ request.user.solde }} €</span></h3> <h3>
<form class="form" method="post"> {% blocktrans %}
{% csrf_token %} Balance : <span class="label label-default">{{ request.user.solde }} €</span>
{% bootstrap_form rechargeform %} {% endblocktrans %}
{% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %} </h3>
</form> <form class="form" method="post">
{% csrf_token %}
{% bootstrap_form rechargeform %}
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %}
</form>
{% endblock %} {% endblock %}

View file

@ -24,40 +24,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load acl %} {% load acl %}
{% load i18n %}
{% block sidebar %} {% block sidebar %}
{% can_change Facture pdf %} {% can_change Facture pdf %}
<a class="list-group-item list-group-item-success" href="{% url "cotisations:new-facture-pdf" %}"> <a class="list-group-item list-group-item-success" href="{% url "cotisations:new-facture-pdf" %}">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i> {% trans "Create an invoice" %}
Créer une facture
</a> </a>
<a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}"> <a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}">
<i class="fa fa-eye"></i> <i class="fa fa-eye"></i> {% trans "Control the invoices" %}
Contrôler les factures
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Facture %} {% can_view_all Facture %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index" %}">
<i class="fa fa-list-ul"></i> <i class="fa fa-list-ul"></i> {% trans "Invoices" %}
Factures
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Article %} {% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> <i class="fa fa-list-ul"></i> {% trans "Available articles" %}
Articles en vente
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Banque %} {% can_view_all Banque %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-banque" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-banque" %}">
<i class="fa fa-list-ul"></i> <i class="fa fa-list-ul"></i> {% trans "Banks" %}
Banques
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all Paiement %} {% can_view_all Paiement %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-paiement" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-paiement" %}">
<i class="fa fa-list-ul"></i> <i class="fa fa-list-ul"></i> {% trans "Payment methods" %}
Moyens de paiement
</a> </a>
{% acl_end %} {% acl_end %}
{% endblock %} {% endblock %}

View file

@ -19,6 +19,10 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""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.loader import get_template
from django.template import Context from django.template import Context
@ -37,6 +41,10 @@ CACHE_TIMEOUT = getattr(settings, 'TEX_CACHE_TIMEOUT', 86400) # 1 day
def render_invoice(request, ctx={}): 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([ filename = '_'.join([
'invoice', 'invoice',
slugify(ctx['asso_name']), slugify(ctx['asso_name']),
@ -50,6 +58,11 @@ def render_invoice(request, ctx={}):
return r return r
def render_tex(request, template, ctx={}): 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) context = Context(ctx)
template = get_template(template) template = get_template(template)
rendered_tpl = template.render(context).encode('utf-8') rendered_tpl = template.render(context).encode('utf-8')

View file

@ -36,6 +36,7 @@ from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.forms import modelformset_factory, formset_factory from django.forms import modelformset_factory, formset_factory
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.debug import sensitive_variables from django.views.decorators.debug import sensitive_variables
# Import des models, forms et fonctions re2o # Import des models, forms et fonctions re2o
@ -72,7 +73,7 @@ from .forms import (
NewFactureSoldeForm, NewFactureSoldeForm,
RechargeForm RechargeForm
) )
from . import payment from . import payment as online_payment
from .tex import render_invoice from .tex import render_invoice
@ -81,114 +82,141 @@ from .tex import render_invoice
@can_create(Facture) @can_create(Facture)
@can_edit(User) @can_edit(User)
def new_facture(request, user, userid): 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 View called to create a new invoice.
pour ajouter des articles. Currently, Send the list of available articles for the user along with
Parse les article et boucle dans le formset puis save les ventes, a formset of a new invoice (based on the `:forms:NewFactureForm()` form.
enfin sauve la facture parente. A bit of JS is used in the template to add articles in a fancier way.
TODO : simplifier cette fonction, déplacer l'intelligence coté models If everything is correct, save each one of the articles, save the
Facture et Vente.""" purchase object associated and finally the newly created invoice.
facture = Facture(user=user)
# Le template a besoin de connaitre les articles pour le js 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( article_list = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name) Q(type_user='All') | Q(type_user=request.user.class_name)
) )
# On envoie la form fature et un formset d'articles # Building the invocie form and the article formset
facture_form = NewFactureForm(request.POST or None, instance=facture) invoice_form = NewFactureForm(request.POST or None, instance=invoice)
if request.user.is_class_club: if request.user.is_class_club:
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
else: else:
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) 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 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): if any(art.cleaned_data for art in articles):
user_solde = OptionalUser.get_cached_value('user_solde') user_balance = OptionalUser.get_cached_value('user_solde')
solde_negatif = OptionalUser.get_cached_value('solde_negatif') negative_balance = OptionalUser.get_cached_value('solde_negatif')
# Si on paye par solde, que l'option est activée, # If the paiement using balance has been activated,
# on vérifie que le négatif n'est pas atteint # checking that the total price won't get the user under
if user_solde: # the authorized minimum (negative_balance)
if new_facture_instance.paiement == Paiement.objects.get_or_create( if user_balance:
# TODO : change Paiement to Payment
if new_invoice_instance.paiement == Paiement.objects.get_or_create(
moyen='solde' moyen='solde'
)[0]: )[0]:
prix_total = 0 total_price = 0
for art_item in articles: for art_item in articles:
if art_item.cleaned_data: 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'] .prix*art_item.cleaned_data['quantity']
if float(user.solde) - float(prix_total) < solde_negatif: if float(user.solde) - float(total_price) < negative_balance:
messages.error(request, "Le solde est insuffisant pour\ messages.error(
effectuer l'opération") request,
_("Your balance is too low for this operation.")
)
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': userid} 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: for art_item in articles:
if art_item.cleaned_data: if art_item.cleaned_data:
article = art_item.cleaned_data['article'] article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity'] quantity = art_item.cleaned_data['quantity']
new_vente = Vente.objects.create( new_purchase = Vente.objects.create(
facture=new_facture_instance, facture=new_invoice_instance,
name=article.name, name=article.name,
prix=article.prix, prix=article.prix,
type_cotisation=article.type_cotisation, type_cotisation=article.type_cotisation,
duration=article.duration, duration=article.duration,
number=quantity 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 if any(art_item.cleaned_data['article'].type_cotisation
for art_item in articles if art_item.cleaned_data): for art_item in articles if art_item.cleaned_data):
messages.success( messages.success(
request, request,
"La cotisation a été prolongée\ _("The cotisation of %(member_name)s has been \
pour l'adhérent %s jusqu'au %s" % ( extended to %(end_date)s.") % {
user.pseudo, user.end_adhesion() member_name: user.pseudo,
) end_date: user.end_adhesion()
) }
)
# Else, only tell the invoice was created
else: else:
messages.success(request, "La facture a été crée") messages.success(
request,
_("The invoice has been created.")
)
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': userid} kwargs={'userid': userid}
)) ))
messages.error( messages.error(
request, request,
u"Il faut au moins un article valide pour créer une facture" _("You need to choose at least one article.")
) )
return form({ return form(
'factureform': facture_form, {
'venteform': article_formset, 'factureform': invoice_form,
'articlelist': article_list 'venteform': article_formset,
}, 'cotisations/new_facture.html', request) 'articlelist': article_list
},
'cotisations/new_facture.html', request
)
# TODO : change facture to invoice
@login_required @login_required
@can_change(Facture, 'pdf') @can_change(Facture, 'pdf')
def new_facture_pdf(request): 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 View used to generate a custom PDF invoice. It's mainly used to
Vente ou Facture correspondant en bdd""" get invoices that are not taken into account, for the administrative
facture_form = NewFactureFormPdf(request.POST or None) point of view.
if facture_form.is_valid(): """
invoice_form = NewFactureFormPdf(request.POST or None)
if invoice_form.is_valid():
tbl = [] tbl = []
article = facture_form.cleaned_data['article'] article = facture_form.cleaned_data['article']
quantite = facture_form.cleaned_data['number'] quantity = facture_form.cleaned_data['number']
paid = facture_form.cleaned_data['paid'] paid = facture_form.cleaned_data['paid']
destinataire = facture_form.cleaned_data['dest'] recipient = facture_form.cleaned_data['dest']
chambre = facture_form.cleaned_data['chambre'] room = facture_form.cleaned_data['chambre']
fid = facture_form.cleaned_data['fid'] invoice_id = facture_form.cleaned_data['fid']
for art in article: for art in article:
tbl.append([art, quantite, art.prix * quantite]) tbl.append([art, quantity, art.prix * quantity])
prix_total = sum(a[2] for a in tbl) total_price = sum(a[2] for a in tbl)
user = {'name': destinataire, 'room': chambre} user = {'name': recipient, 'room': room}
return render_invoice(request, { return render_invoice(request, {
'DATE': timezone.now(), 'DATE': timezone.now(),
'dest': user, 'dest': user,
'fid': fid, 'fid': invoice_id,
'article': tbl, 'article': tbl,
'total': prix_total, 'total': total_price,
'paid': paid, 'paid': paid,
'asso_name': AssoOption.get_cached_value('name'), 'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'), '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) 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
}) })
return form({ return form({
'factureform': facture_form, 'factureform': invoice_form,
'action_name' : 'Editer' 'action_name': _("Edit")
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
# TODO : change facture to invoice
@login_required @login_required
@can_view(Facture) @can_view(Facture)
def facture_pdf(request, facture, factureid): 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 View used to generate a PDF file from an existing invoice in database
de l'adhérent, etc. Réservée à self pour un user sans droits, Creates a line for each Purchase (thus article sold) and generate the
les droits cableurs permettent d'afficher toute facture""" invoice with the total price, the payment method, the address and the
legal information for the user.
ventes_objects = Vente.objects.all().filter(facture=facture) """
ventes = [] # TODO : change vente to purchase
for vente in ventes_objects: purchases_objects = Vente.objects.all().filter(facture=facture)
ventes.append([vente, vente.number, vente.prix_total]) purchases = []
for purchase in purchases_objects:
purchases.append([purchase, purchase.number, purchase.prix_total])
return render_invoice(request, { return render_invoice(request, {
'paid': True, 'paid': True,
'fid': facture.id, 'fid': facture.id,
'DATE': facture.date, 'DATE': facture.date,
'dest': facture.user, 'dest': facture.user,
'article': ventes, 'article': purchases,
'total': facture.prix_total(), 'total': facture.prix_total(),
'asso_name': AssoOption.get_cached_value('name'), 'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'), 'line1': AssoOption.get_cached_value('adresse1'),
@ -233,302 +264,434 @@ def facture_pdf(request, facture, factureid):
}) })
# TODO : change facture to invoice
@login_required @login_required
@can_edit(Facture) @can_edit(Facture)
def edit_facture(request, facture, factureid): 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 View used to edit an existing invoice.
en bois etc). Mets à jour les durée de cotisation attenantes""" Articles can be added or remove to the invoice and quantity
facture_form = EditFactureForm(request.POST or None, instance=facture, user=request.user) can be set as desired. This is also the view used to invalidate
ventes_objects = Vente.objects.filter(facture=facture) an invoice.
vente_form_set = modelformset_factory( """
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, Vente,
fields=('name', 'number'), fields=('name', 'number'),
extra=0, 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 redirect(reverse('cotisations:index'))
return form({ return form({
'factureform': facture_form, 'factureform': invoice_form,
'venteform': vente_form 'venteform': purchase_form
}, 'cotisations/edit_facture.html', request) }, 'cotisations/edit_facture.html', request)
# TODO : change facture to invoice
@login_required @login_required
@can_delete(Facture) @can_delete(Facture)
def del_facture(request, facture, factureid): 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": if request.method == "POST":
facture.delete() 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 redirect(reverse('cotisations:index'))
return form({ return form({
'objet': facture, 'objet': facture,
'objet_name': 'facture' 'objet_name': _("Invoice")
}, 'cotisations/delete.html', request) }, 'cotisations/delete.html', request)
# TODO : change solde to balance
@login_required @login_required
@can_create(Facture) @can_create(Facture)
@can_edit(User) @can_edit(User)
def credit_solde(request, user, userid): def credit_solde(request, user, userid):
""" Credit ou débit de solde """ """
facture = CreditSoldeForm(request.POST or None) View used to edit the balance of a user.
if facture.is_valid(): Can be use either to increase or decrease a user's balance.
facture_instance = facture.save(commit=False) """
facture_instance.user = user # TODO : change facture to invoice
facture_instance.save() invoice = CreditSoldeForm(request.POST or None)
new_vente = Vente.objects.create( if invoice.is_valid():
facture=facture_instance, invoice_instance = invoice.save(commit=False)
invoice_instance.user = user
invoice_instance.save()
new_purchase = Vente.objects.create(
facture=invoice_instance,
name="solde", name="solde",
prix=facture.cleaned_data['montant'], prix=invoice.cleaned_data['montant'],
number=1 number=1
) )
new_vente.save() new_purchase.save()
messages.success(request, "Solde modifié") messages.success(
request,
_("Balance successfully updated.")
)
return redirect(reverse('cotisations:index')) 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 @login_required
@can_create(Article) @can_create(Article)
def add_article(request): def add_article(request):
"""Ajoute un article. Champs : désignation, """
prix, est-ce une cotisation et si oui sa durée View used to add an article.
Réservé au trésorier
Nota bene : les ventes déjà effectuées ne sont pas reliées .. note:: If a purchase has already been sold, the price are calculated
aux articles en vente. La désignation, le prix... sont once and for all. That means even if the price of an article is edited
copiés à la création de la facture. Un changement de prix n'a later, it won't change the invoice. That is really important to keep
PAS de conséquence sur les ventes déjà faites""" this behaviour in order not to modify all the past and already
accepted invoices.
"""
article = ArticleForm(request.POST or None) article = ArticleForm(request.POST or None)
if article.is_valid(): if article.is_valid():
article.save() 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 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 @login_required
@can_edit(Article) @can_edit(Article)
def edit_article(request, article_instance, articleid): 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) article = ArticleForm(request.POST or None, instance=article_instance)
if article.is_valid(): if article.is_valid():
if article.changed_data: if article.changed_data:
article.save() 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 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 @login_required
@can_delete_set(Article) @can_delete_set(Article)
def del_article(request, instances): 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) article = DelArticleForm(request.POST or None, instances=instances)
if article.is_valid(): if article.is_valid():
article_del = article.cleaned_data['articles'] article_del = article.cleaned_data['articles']
article_del.delete() 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 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 @login_required
@can_create(Paiement) @can_create(Paiement)
def add_paiement(request): def add_paiement(request):
"""Ajoute un moyen de paiement. Relié aux factures """
via foreign key""" View used to add a payment method.
paiement = PaiementForm(request.POST or None) """
if paiement.is_valid(): payment = PaiementForm(request.POST or None)
paiement.save() if payment.is_valid():
messages.success(request, "Le moyen de paiement a été ajouté") payment.save()
messages.success(
request,
_("The payment method has been successfully created.")
)
return redirect(reverse('cotisations:index-paiement')) 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 @login_required
@can_edit(Paiement) @can_edit(Paiement)
def edit_paiement(request, paiement_instance, paiementid): def edit_paiement(request, paiement_instance, paiementid):
"""Edition d'un moyen de paiement""" """
paiement = PaiementForm(request.POST or None, instance=paiement_instance) View used to edit a payment method.
if paiement.is_valid(): """
if paiement.changed_data: payment = PaiementForm(request.POST or None, instance=paiement_instance)
paiement.save() if payment.is_valid():
messages.success(request, "Type de paiement modifié") if payment.changed_data:
payment.save()
messages.success(
request,
_("The payement method has been successfully edited.")
)
return redirect(reverse('cotisations:index-paiement')) 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 @login_required
@can_delete_set(Paiement) @can_delete_set(Paiement)
def del_paiement(request, instances): def del_paiement(request, instances):
"""Suppression d'un moyen de paiement""" """
paiement = DelPaiementForm(request.POST or None, instances=instances) View used to delete a set of payment methods.
if paiement.is_valid(): """
paiement_dels = paiement.cleaned_data['paiements'] payment = DelPaiementForm(request.POST or None, instances=instances)
for paiement_del in paiement_dels: if payment.is_valid():
payment_dels = payment.cleaned_data['paiements']
for payment_del in payment_dels:
try: try:
paiement_del.delete() payment_del.delete()
messages.success( messages.success(
request, request,
"Le moyen de paiement a été supprimé" _("The payment method %(method_name)s has been \
) successfully deleted.") % {
method_name: payment_del
}
)
except ProtectedError: except ProtectedError:
messages.error( messages.error(
request, request,
"Le moyen de paiement %s est affecté à au moins une\ _("The payment method %(method_name)s can't be deleted \
facture, vous ne pouvez pas le supprimer" % paiement_del because there are invoices using it.") % {
method_name: payment_del
}
) )
return redirect(reverse('cotisations:index-paiement')) 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 @login_required
@can_create(Banque) @can_create(Banque)
def add_banque(request): def add_banque(request):
"""Ajoute une banque à la liste des banques""" """
banque = BanqueForm(request.POST or None) View used to add a bank.
if banque.is_valid(): """
banque.save() bank = BanqueForm(request.POST or None)
messages.success(request, "La banque a été ajoutée") if bank.is_valid():
bank.save()
messages.success(
request,
_("The bank has been successfully created.")
)
return redirect(reverse('cotisations:index-banque')) 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 @login_required
@can_edit(Banque) @can_edit(Banque)
def edit_banque(request, banque_instance, banqueid): def edit_banque(request, banque_instance, banqueid):
"""Edite le nom d'une banque""" """
banque = BanqueForm(request.POST or None, instance=banque_instance) View used to edit a bank.
if banque.is_valid(): """
if banque.changed_data: bank = BanqueForm(request.POST or None, instance=banque_instance)
banque.save() if bank.is_valid():
messages.success(request, "Banque modifiée") if bank.changed_data:
bank.save()
messages.success(
request,
_("The bank has been successfully edited")
)
return redirect(reverse('cotisations:index-banque')) 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 @login_required
@can_delete_set(Banque) @can_delete_set(Banque)
def del_banque(request, instances): def del_banque(request, instances):
"""Supprime une banque""" """
banque = DelBanqueForm(request.POST or None, instances=instances) View used to delete a set of banks.
if banque.is_valid(): """
banque_dels = banque.cleaned_data['banques'] bank = DelBanqueForm(request.POST or None, instances=instances)
for banque_del in banque_dels: if bank.is_valid():
bank_dels = bank.cleaned_data['banques']
for bank_del in bank_dels:
try: try:
banque_del.delete() bank_del.delete()
messages.success(request, "La banque a été supprimée") messages.success(
request,
_("The bank %(bank_name)s has been successfully \
deleted.") % {
bank_name: bank_del
}
)
except ProtectedError: except ProtectedError:
messages.error(request, "La banque %s est affectée à au moins\ messages.error(
une facture, vous ne pouvez pas la supprimer" % banque_del) 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 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 @login_required
@can_view_all(Facture) @can_view_all(Facture)
@can_change(Facture, 'control') @can_change(Facture, 'control')
def control(request): 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') pagination_number = GeneralOption.get_cached_value('pagination_number')
facture_list = Facture.objects.select_related('user').select_related('paiement') invoice_list = Facture.objects.select_related('user').select_related('paiement')
facture_list = SortTable.sort( invoice_list = SortTable.sort(
facture_list, invoice_list,
request.GET.get('col'), request.GET.get('col'),
request.GET.get('order'), request.GET.get('order'),
SortTable.COTISATIONS_CONTROL SortTable.COTISATIONS_CONTROL
) )
controlform_set = modelformset_factory( control_invoices_formset = modelformset_factory(
Facture, Facture,
fields=('control', 'valid'), fields=('control', 'valid'),
extra=0 extra=0
) )
facture_list = re2o_paginator(request, facture_list, pagination_number) invoice_list = re2o_paginator(request, invoice_list, pagination_number)
controlform = controlform_set(request.POST or None, queryset=facture_list.object_list) control_invoices_form = control_invoices_formset(
if controlform.is_valid(): request.POST or None,
controlform.save() queryset=invoice_list.object_list
)
if control_invoices_form.is_valid():
control_invoices_form.save()
reversion.set_comment("Controle") reversion.set_comment("Controle")
return redirect(reverse('cotisations:control')) return redirect(reverse('cotisations:control'))
return render(request, 'cotisations/control.html', { return render(request, 'cotisations/control.html', {
'facture_list': facture_list, 'facture_list': invoice_list,
'controlform': controlform 'controlform': control_invoices_form
}) })
@login_required @login_required
@can_view_all(Article) @can_view_all(Article)
def index_article(request): 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') article_list = Article.objects.order_by('name')
return render(request, 'cotisations/index_article.html', { return render(request, 'cotisations/index_article.html', {
'article_list': article_list 'article_list': article_list
}) })
# TODO : change paiement to payment
@login_required @login_required
@can_view_all(Paiement) @can_view_all(Paiement)
def index_paiement(request): 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', { return render(request, 'cotisations/index_paiement.html', {
'paiement_list': paiement_list 'paiement_list': payment_list
}) })
# TODO : change banque to bank
@login_required @login_required
@can_view_all(Banque) @can_view_all(Banque)
def index_banque(request): 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', { return render(request, 'cotisations/index_banque.html', {
'banque_list': banque_list 'banque_list': bank_list
}) })
@login_required @login_required
@can_view_all(Facture) @can_view_all(Facture)
def index(request): 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') 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') .select_related('paiement').prefetch_related('vente_set')
facture_list = SortTable.sort( invoice_list = SortTable.sort(
facture_list, invoice_list,
request.GET.get('col'), request.GET.get('col'),
request.GET.get('order'), request.GET.get('order'),
SortTable.COTISATIONS_INDEX 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', { 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 @login_required
def new_facture_solde(request, userid): 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 View called to create a new invoice when using the balance to pay.
pour ajouter des articles. Currently, send the list of available articles for the user along with
Parse les article et boucle dans le formset puis save les ventes, a formset of a new invoice (based on the `:forms:NewFactureForm()` form.
enfin sauve la facture parente. A bit of JS is used in the template to add articles in a fancier way.
TODO : simplifier cette fonction, déplacer l'intelligence coté models If everything is correct, save each one of the articles, save the
Facture et Vente.""" 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 user = request.user
facture = Facture(user=user) invoice = Facture(user=user)
paiement, _created = Paiement.objects.get_or_create(moyen='Solde') payment, _created = Paiement.objects.get_or_create(moyen='Solde')
facture.paiement = paiement facture.paiement = payment
# Le template a besoin de connaitre les articles pour le js # The template needs the list of articles (for the JS part)
article_list = Article.objects.filter( article_list = Article.objects.filter(
Q(type_user='All') | Q(type_user=request.user.class_name) Q(type_user='All') | Q(type_user=request.user.class_name)
) )
@ -536,60 +699,75 @@ def new_facture_solde(request, userid):
article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) article_formset = formset_factory(SelectClubArticleForm)(request.POST or None)
else: else:
article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) article_formset = formset_factory(SelectUserArticleForm)(request.POST or None)
if article_formset.is_valid(): if article_formset.is_valid():
articles = article_formset 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): if any(art.cleaned_data for art in articles):
user_solde = OptionalUser.get_cached_value('user_solde') user_balance = OptionalUser.get_cached_value('user_solde')
solde_negatif = OptionalUser.get_cached_value('solde_negatif') negative_balance = OptionalUser.get_cached_value('solde_negatif')
# Si on paye par solde, que l'option est activée, # If the paiement using balance has been activated,
# on vérifie que le négatif n'est pas atteint # checking that the total price won't get the user under
if user_solde: # the authorized minimum (negative_balance)
prix_total = 0 if user_balance:
total_price = 0
for art_item in articles: for art_item in articles:
if art_item.cleaned_data: 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'] .prix*art_item.cleaned_data['quantity']
if float(user.solde) - float(prix_total) < solde_negatif: if float(user.solde) - float(total_price) < negative_balance:
messages.error(request, "Le solde est insuffisant pour\ messages.error(
effectuer l'opération") request,
_("The balance is too low for this operation.")
)
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': userid} kwargs={'userid': userid}
)) ))
facture.save() # Saving the invoice
invoice.save()
# Building a purchase for each article sold
for art_item in articles: for art_item in articles:
if art_item.cleaned_data: if art_item.cleaned_data:
article = art_item.cleaned_data['article'] article = art_item.cleaned_data['article']
quantity = art_item.cleaned_data['quantity'] quantity = art_item.cleaned_data['quantity']
new_vente = Vente.objects.create( new_purchase = Vente.objects.create(
facture=facture, facture=invoice,
name=article.name, name=article.name,
prix=article.prix, prix=article.prix,
type_cotisation=article.type_cotisation, type_cotisation=article.type_cotisation,
duration=article.duration, duration=article.duration,
number=quantity 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 if any(art_item.cleaned_data['article'].type_cotisation
for art_item in articles if art_item.cleaned_data): for art_item in articles if art_item.cleaned_data):
messages.success( messages.success(
request, request,
"La cotisation a été prolongée\ _("The cotisation of %(member_name)s has been successfully \
pour l'adhérent %s jusqu'au %s" % ( extended to %(end_date)s.") % {
user.pseudo, user.end_adhesion() member_name: user.pseudo,
) end_date: user.end_adhesion()
) }
)
# Else, only tell the invoice was created
else: else:
messages.success(request, "La facture a été crée") messages.success(
request,
_("The invoice has been successuflly created.")
)
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': userid} kwargs={'userid': userid}
)) ))
messages.error( messages.error(
request, 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( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': userid} kwargs={'userid': userid}
@ -601,31 +779,37 @@ def new_facture_solde(request, userid):
}, 'cotisations/new_facture_solde.html', request) }, 'cotisations/new_facture_solde.html', request)
# TODO : change recharge to refill
@login_required @login_required
def recharge(request): def recharge(request):
"""
View used to refill the balance by using online payment.
"""
if AssoOption.get_cached_value('payment') == 'NONE': if AssoOption.get_cached_value('payment') == 'NONE':
messages.error( messages.error(
request, request,
"Le paiement en ligne est désactivé." _("Online payment is disabled.")
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': request.user.id} kwargs={'userid': request.user.id}
)) ))
f = RechargeForm(request.POST or None, user=request.user) refill_form = RechargeForm(request.POST or None, user=request.user)
if f.is_valid(): if refill_form.is_valid():
facture = Facture(user=request.user) invoice = Facture(user=request.user)
paiement, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne') payment, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne')
facture.paiement = paiement facture.paiement = payment
facture.valid = False facture.valid = False
facture.save() facture.save()
v = Vente.objects.create( purchase = Vente.objects.create(
facture=facture, facture=invoice,
name='solde', name='solde',
prix=f.cleaned_data['value'], prix=refill_form.cleaned_data['value'],
number=1, number=1
) )
v.save() purchase.save()
content = payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](facture, request) content = online_payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](invoice, request)
return render(request, 'cotisations/payment.html', content) return render(request, 'cotisations/payment.html', content)
return form({'rechargeform':f}, 'cotisations/recharge.html', request) return form({
'rechargeform': refill_form
}, 'cotisations/recharge.html', request)

View file

@ -82,6 +82,7 @@ INSTALLED_APPS = (
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
@ -120,7 +121,7 @@ WSGI_APPLICATION = 're2o.wsgi.application'
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/ # https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'fr-fr' LANGUAGE_CODE = 'en'
TIME_ZONE = 'Europe/Paris' TIME_ZONE = 'Europe/Paris'