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

Translation : translate docstrings of cotisations

This commit is contained in:
Maël Kervella 2018-04-09 17:40:46 +00:00
parent 8da337c549
commit f2f4336e87
5 changed files with 541 additions and 366 deletions

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
@ -51,9 +50,10 @@ 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
# TODO : translate doc string in English 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)
@ -91,8 +91,10 @@ class NewFactureForm(FormRevMixin, ModelForm):
class CreditSoldeForm(NewFactureForm): class CreditSoldeForm(NewFactureForm):
"""Permet de faire des opérations sur le solde si il est activé""" """
# TODO : translate docstring to English 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']
@ -108,8 +110,9 @@ class CreditSoldeForm(NewFactureForm):
class SelectUserArticleForm(FormRevMixin, Form): class SelectUserArticleForm(FormRevMixin, Form):
"""Selection d'un article lors de la creation d'une facture""" """
# TODO : translate docstring to English 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=_l("Article"), label=_l("Article"),
@ -123,8 +126,9 @@ class SelectUserArticleForm(FormRevMixin, Form):
class SelectClubArticleForm(Form): class SelectClubArticleForm(Form):
"""Selection d'un article lors de la creation d'une facture""" """
# TODO : translate docstring to English 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=_l("Article"), label=_l("Article"),
@ -138,8 +142,9 @@ class SelectClubArticleForm(Form):
# TODO : change Facture to Invoice # TODO : change Facture to Invoice
class NewFactureFormPdf(Form): class NewFactureFormPdf(Form):
"""Creation d'un pdf facture par le trésorier""" """
# TODO : translate docstring to English Form used to create a custom PDF invoice.
"""
article = forms.ModelMultipleChoiceField( article = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(), queryset=Article.objects.all(),
label=_l("Article") label=_l("Article")
@ -162,8 +167,10 @@ class NewFactureFormPdf(Form):
# TODO : change Facture to Invoice # TODO : change Facture to Invoice
class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): class EditFactureForm(FieldPermissionFormMixin, NewFactureForm):
"""Edition d'une facture : moyen de paiement, banque, user parent""" """
# TODO : translate docstring to English 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 # TODO : change Facture to Invoice
model = Facture model = Facture
@ -179,8 +186,9 @@ class EditFactureForm(FieldPermissionFormMixin, NewFactureForm):
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):
"""Creation d'un article. Champs : nom, cotisation, durée""" """
# TODO : translate docstring to English Form used to create an article.
"""
class Meta: class Meta:
model = Article model = Article
fields = '__all__' fields = '__all__'
@ -192,9 +200,10 @@ class ArticleForm(FormRevMixin, ModelForm):
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.
# TODO : translate docstring to English 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=_l("Existing articles"), label=_l("Existing articles"),
@ -212,9 +221,11 @@ class DelArticleForm(FormRevMixin, Form):
# TODO : change Paiement to Payment # 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.
# TODO : translate docstring to English 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 # TODO : change moyen to method and type_paiement to payment_type
@ -233,9 +244,10 @@ class PaiementForm(FormRevMixin, ModelForm):
# TODO : change paiement to payment # 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.
# TODO : translate docstring to English The user must choose the one to delete by checking the boxes.
"""
# TODO : change paiement to payment # TODO : change paiement to payment
paiements = forms.ModelMultipleChoiceField( paiements = forms.ModelMultipleChoiceField(
queryset=Paiement.objects.none(), queryset=Paiement.objects.none(),
@ -254,8 +266,9 @@ class DelPaiementForm(FormRevMixin, Form):
# TODO : change banque to bank # TODO : change banque to bank
class BanqueForm(FormRevMixin, ModelForm): class BanqueForm(FormRevMixin, ModelForm):
"""Creation d'une banque, field name""" """
# TODO : translate docstring to Englishh Form used to create a bank.
"""
class Meta: class Meta:
# TODO : change banque to bank # TODO : change banque to bank
model = Banque model = Banque
@ -269,8 +282,10 @@ class BanqueForm(FormRevMixin, ModelForm):
# TODO : change banque to bank # TODO : change banque to bank
class DelBanqueForm(FormRevMixin, Form): class DelBanqueForm(FormRevMixin, Form):
"""Selection d'une ou plusieurs banques, pour suppression""" """
# TODO : translate docstrign to English 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 # TODO : change banque to bank
banques = forms.ModelMultipleChoiceField( banques = forms.ModelMultipleChoiceField(
queryset=Banque.objects.none(), queryset=Banque.objects.none(),
@ -289,9 +304,9 @@ class DelBanqueForm(FormRevMixin, Form):
# TODO : change facture to Invoice # 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
# TODO : translate docstring to English """
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
@ -335,6 +350,9 @@ class NewFactureSoldeForm(NewFactureForm):
# TODO : Better name and docstring # 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=_l("Amount"), label=_l("Amount"),
min_value=0.01, min_value=0.01,

View file

@ -21,28 +21,14 @@
# 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
""" """
# TODO : translate docstring to English
from __future__ import unicode_literals from __future__ import unicode_literals
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@ -65,11 +51,24 @@ from re2o.mixins import AclMixin, RevMixin
# TODO : change facture to invoice # 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)"""
# TODO : translate docstrign to English 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 # TODO : change paiement to payment
@ -122,20 +121,21 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
# TODO : change prix to price # 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.
# TODO : translate docstring to English Deprecated, use :total_price instead.
# TODO : change prix to price """
prix = Vente.objects.filter( 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 # 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
# TODO : translate docstrign to English and take the quantities into account.
"""
# TODO : change Vente to somethingelse # TODO : change Vente to somethingelse
return Vente.objects.filter( return Vente.objects.filter(
facture=self facture=self
@ -147,8 +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""" """
# TODO : translate docstring to English 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))
@ -204,8 +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""" """
# TODO : translate docstrign into English 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)
@ -213,18 +216,26 @@ 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""" """
# TODO : translate docstring into English 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 # 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.
# TODO : translate docstring into English
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 # TODO : change this to English
COTISATION_TYPE = ( COTISATION_TYPE = (
@ -281,15 +292,16 @@ class Vente(RevMixin, AclMixin, models.Model):
# TODO : change prix_total to total_price # TODO : change prix_total to total_price
def prix_total(self): def prix_total(self):
"""Renvoie le prix_total de self (nombre*prix)""" """
# TODO : translate docstring to english 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.
# TODO : translate docstring to English """
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(
@ -297,10 +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
# TODO : translate docstring to English 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
@ -328,8 +341,12 @@ class Vente(RevMixin, AclMixin, models.Model):
return return
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# TODO : ecrire une docstring """
# 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( raise ValidationError(
_("A cotisation should always have a duration.") _("A cotisation should always have a duration.")
@ -372,38 +389,44 @@ class Vente(RevMixin, AclMixin, models.Model):
# TODO : change vente to purchase # 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
# TODO : translate docstring to English LDAP user when a purchase has been saved.
# TODO : change vente to purchase """
vente = kwargs['instance'] 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 # 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.
# TODO : translate docstring to English """
# TODO : change vente to purchase purchase = kwargs['instance']
vente = kwargs['instance'] if purchase.type_cotisation:
if vente.type_cotisation: user = purchase.facture.user
user = vente.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.
# TODO : translate docstring to English
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 # TODO : Either use TYPE or TYPES in both choices but not both
USER_TYPES = ( USER_TYPES = (
@ -473,8 +496,13 @@ class Article(RevMixin, AclMixin, models.Model):
class Banque(RevMixin, AclMixin, models.Model): class Banque(RevMixin, AclMixin, models.Model):
"""Liste des banques""" """
# TODO : translate docstring to English 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( name = models.CharField(
max_length=255, max_length=255,
@ -494,8 +522,14 @@ class Banque(RevMixin, AclMixin, models.Model):
# TODO : change Paiement to Payment # TODO : change Paiement to Payment
class Paiement(RevMixin, AclMixin, models.Model): class Paiement(RevMixin, AclMixin, models.Model):
"""Moyens de paiement""" """
# TODO : translate docstring to English 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, _l("Standard")), (0, _l("Standard")),
@ -524,11 +558,16 @@ class Paiement(RevMixin, AclMixin, models.Model):
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...""" """
# TODO : translate docstring to English 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( raise ValidationError(
_("You cannot have multiple payment method of type cheque") _("You cannot have multiple payment method of type cheque")
@ -537,8 +576,16 @@ class Paiement(RevMixin, AclMixin, models.Model):
class Cotisation(RevMixin, AclMixin, models.Model): class Cotisation(RevMixin, AclMixin, models.Model):
"""Objet cotisation, debut et fin, relié en onetoone à une vente""" """
# TODO : translate docstring to English 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', _l("Connexion")), ('Connexion', _l("Connexion")),
@ -602,8 +649,10 @@ 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""" """
# TODO : translate docstring to English 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')
@ -613,8 +662,10 @@ def cotisation_post_save(sender, **kwargs):
# TODO : should be name cotisation_post_delete # 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""" """
# TODO : translate docstring to English 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

@ -20,6 +20,9 @@ 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,
@ -33,6 +36,9 @@ 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,
_("The payment has been refused.") _("The payment has been refused.")
@ -41,6 +47,11 @@ def refuse_payment(request):
@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:
@ -55,8 +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.
# TODO : translate comment to English
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")
@ -67,23 +77,28 @@ def ipn(request):
facture = get_object_or_404(Facture, id=factureid) facture = get_object_or_404(Facture, id=factureid)
# TODO : translate comments to English # Checking that the payment is valid
# On vérifie que le paiement est valide
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')),
@ -110,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

@ -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

@ -73,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
@ -82,50 +82,51 @@ 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.
# TODO : translate docstring to English
# TODO : change facture to invoice TODO : The whole verification process should be moved to the model. This
facture = Facture(user=user) function should only act as a dumb interface between the model and the
# TODO : change comment to English user.
# Le template a besoin de connaitre les articles pour le js """
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
# TODO : change facture to invoice invoice_form = NewFactureForm(request.POST or None, instance=invoice)
facture_form = NewFactureForm(request.POST or None, instance=facture)
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):
# TODO : change solde to balance user_balance = OptionalUser.get_cached_value('user_solde')
user_solde = OptionalUser.get_cached_value('user_solde') negative_balance = OptionalUser.get_cached_value('solde_negatif')
solde_negatif = OptionalUser.get_cached_value('solde_negatif') # If the paiement using balance has been activated,
# Si on paye par solde, que l'option est activée, # checking that the total price won't get the user under
# on vérifie que le négatif n'est pas atteint # the authorized minimum (negative_balance)
if user_solde: if user_balance:
# TODO : change Paiement to Payment # TODO : change Paiement to Payment
if new_facture_instance.paiement == Paiement.objects.get_or_create( 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:
# change prix to price total_price += art_item.cleaned_data['article']\
prix_total += 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( messages.error(
request, request,
_("Your balance is too low for this operation.") _("Your balance is too low for this operation.")
@ -134,20 +135,26 @@ def new_facture(request, user, userid):
'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(
@ -158,6 +165,7 @@ def new_facture(request, user, userid):
end_date: user.end_adhesion() end_date: user.end_adhesion()
} }
) )
# Else, only tell the invoice was created
else: else:
messages.success( messages.success(
request, request,
@ -173,7 +181,7 @@ def new_facture(request, user, userid):
) )
return form( return form(
{ {
'factureform': facture_form, 'factureform': invoice_form,
'venteform': article_formset, 'venteform': article_formset,
'articlelist': article_list 'articlelist': article_list
}, },
@ -185,29 +193,30 @@ def new_facture(request, user, userid):
@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
# TODO : translate docstring to English point of view.
facture_form = NewFactureFormPdf(request.POST or None) """
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'),
@ -218,8 +227,8 @@ 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)
@ -227,22 +236,23 @@ def new_facture_pdf(request):
@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
# TODO : translate docstring to English legal information for the user.
"""
# TODO : change vente to purchase # TODO : change vente to purchase
ventes_objects = Vente.objects.all().filter(facture=facture) purchases_objects = Vente.objects.all().filter(facture=facture)
ventes = [] purchases = []
for vente in ventes_objects: for purchase in purchases_objects:
ventes.append([vente, vente.number, vente.prix_total]) 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'),
@ -258,31 +268,33 @@ def facture_pdf(request, facture, factureid):
@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
# TODO : translate docstring to English can be set as desired. This is also the view used to invalidate
facture_form = EditFactureForm(request.POST or None, instance=facture, user=request.user) an invoice.
ventes_objects = Vente.objects.filter(facture=facture) """
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)
) )
vente_form = vente_form_set(request.POST or None, queryset=ventes_objects) purchase_form = purchase_form_set(request.POST or None, queryset=purchases_objects)
if facture_form.is_valid() and vente_form.is_valid(): if invoice_form.is_valid() and purchase_form.is_valid():
if facture_form.changed_data: if invoice_form.changed_data:
facture_form.save() invoice_form.save()
vente_form.save() purchase_form.save()
messages.success( messages.success(
request, request,
_("The invoice has been successfully edited.") _("The invoice has been successfully edited.")
) )
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)
@ -290,10 +302,11 @@ def edit_facture(request, facture, factureid):
@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.
# TODO : translate docstring to English """
if request.method == "POST": if request.method == "POST":
facture.delete()
messages.success( messages.success(
request, request,
_("The invoice has been successfully deleted.") _("The invoice has been successfully deleted.")
@ -301,7 +314,7 @@ def del_facture(request, facture, factureid):
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)
@ -310,40 +323,46 @@ def del_facture(request, facture, factureid):
@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 """ """
# TODO : translate docstring to English View used to edit the balance of a user.
Can be use either to increase or decrease a user's balance.
"""
# TODO : change facture to invoice # TODO : change facture to invoice
facture = CreditSoldeForm(request.POST or None) invoice = CreditSoldeForm(request.POST or None)
if facture.is_valid(): if invoice.is_valid():
facture_instance = facture.save(commit=False) invoice_instance = invoice.save(commit=False)
facture_instance.user = user invoice_instance.user = user
facture_instance.save() invoice_instance.save()
new_vente = Vente.objects.create( new_purchase = Vente.objects.create(
facture=facture_instance, 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( messages.success(
request, request,
_("Balance successfully updated.") _("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
# TODO : translate docstring to English 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()
@ -352,15 +371,18 @@ def add_article(request):
_("The article has been successfully created.") _("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.
# TODO : translate dosctring to English """
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:
@ -370,14 +392,18 @@ def edit_article(request, article_instance, articleid):
_("The article has been successfully edited.") _("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""" """
# TODO : translate docstring to English 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']
@ -387,63 +413,73 @@ def del_article(request, instances):
_("The article(s) have been successfully deleted.") _("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 # 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.
# TODO : translate docstring to English """
# TODO : change paiement to Payment payment = PaiementForm(request.POST or None)
paiement = PaiementForm(request.POST or None) if payment.is_valid():
if paiement.is_valid(): payment.save()
paiement.save()
messages.success( messages.success(
request, request,
_("The payment method has been successfully created.") _("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 # 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""" """
# TODO : translate docstring to English View used to edit a payment method.
paiement = PaiementForm(request.POST or None, instance=paiement_instance) """
if paiement.is_valid(): payment = PaiementForm(request.POST or None, instance=paiement_instance)
if paiement.changed_data: if payment.is_valid():
paiement.save() if payment.changed_data:
payment.save()
messages.success( messages.success(
request, request,
_("The payement method has been successfully edited.") _("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 # 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""" """
# TODO : translate docstring to English View used to delete a set of payment methods.
paiement = DelPaiementForm(request.POST or None, instances=instances) """
if paiement.is_valid(): payment = DelPaiementForm(request.POST or None, instances=instances)
paiement_dels = paiement.cleaned_data['paiements'] if payment.is_valid():
for paiement_del in paiement_dels: 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,
_("The payment method %(method_name)s has been \ _("The payment method %(method_name)s has been \
successfully deleted.") % { successfully deleted.") % {
method_name: paiement_del method_name: payment_del
} }
) )
except ProtectedError: except ProtectedError:
@ -451,65 +487,77 @@ def del_paiement(request, instances):
request, request,
_("The payment method %(method_name)s can't be deleted \ _("The payment method %(method_name)s can't be deleted \
because there are invoices using it.") % { because there are invoices using it.") % {
method_name: paiement_del 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 # 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""" """
# TODO : tranlate docstring to English View used to add a bank.
banque = BanqueForm(request.POST or None) """
if banque.is_valid(): bank = BanqueForm(request.POST or None)
banque.save() if bank.is_valid():
bank.save()
messages.success( messages.success(
request, request,
_("The bank has been successfully created.") _("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 # 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""" """
# TODO : translate docstring to English View used to edit a bank.
banque = BanqueForm(request.POST or None, instance=banque_instance) """
if banque.is_valid(): bank = BanqueForm(request.POST or None, instance=banque_instance)
if banque.changed_data: if bank.is_valid():
banque.save() if bank.changed_data:
bank.save()
messages.success( messages.success(
request, request,
_("The bank has been successfully edited") _("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 # 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""" """
# TODO : translate docstring to English View used to delete a set of banks.
banque = DelBanqueForm(request.POST or None, instances=instances) """
if banque.is_valid(): bank = DelBanqueForm(request.POST or None, instances=instances)
banque_dels = banque.cleaned_data['banques'] if bank.is_valid():
for banque_del in banque_dels: bank_dels = bank.cleaned_data['banques']
for bank_del in bank_dels:
try: try:
banque_del.delete() bank_del.delete()
messages.success( messages.success(
request, request,
_("The bank %(bank_name)s has been successfully \ _("The bank %(bank_name)s has been successfully \
deleted.") % { deleted.") % {
bank_name: banque_del bank_name: bank_del
} }
) )
except ProtectedError: except ProtectedError:
@ -517,11 +565,14 @@ def del_banque(request, instances):
request, request,
_("The bank %(bank_name)s can't be deleted \ _("The bank %(bank_name)s can't be deleted \
because there are invoices using it.") % { because there are invoices using it.") % {
bank_name: banque_del 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 # TODO : change facture to invoice
@ -529,39 +580,44 @@ def del_banque(request, instances):
@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.
# TODO : translate docstring to English """
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""" """
# TODO : translate docstrign to English 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
@ -572,11 +628,12 @@ def index_article(request):
@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""" """
# TODO : translate docstring to English View used to display the list of all available payment methods.
paiement_list = Paiement.objects.order_by('moyen') """
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
}) })
@ -584,51 +641,57 @@ def index_paiement(request):
@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""" """
# TODO : translate docstring to English View used to display the list of all available banks.
banque_list = Banque.objects.order_by('name') """
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 +""" """
# TODO : translate docstring to English 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 # 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 : translate docstring to English
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
# TODO : translate comments to English # The template needs the list of articles (for the JS part)
# Le template a besoin de connaitre les articles pour le js
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)
) )
@ -636,21 +699,23 @@ 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( messages.error(
request, request,
_("The balance is too low for this operation.") _("The balance is too low for this operation.")
@ -659,20 +724,26 @@ def new_facture_solde(request, userid):
'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(
@ -683,6 +754,7 @@ def new_facture_solde(request, userid):
end_date: user.end_adhesion() end_date: user.end_adhesion()
} }
) )
# Else, only tell the invoice was created
else: else:
messages.success( messages.success(
request, request,
@ -707,9 +779,12 @@ def new_facture_solde(request, userid):
}, 'cotisations/new_facture_solde.html', request) }, 'cotisations/new_facture_solde.html', request)
# TODO : change recharge to reload # 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,
@ -719,20 +794,22 @@ def recharge(request):
'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)