diff --git a/cotisations/forms.py b/cotisations/forms.py index 5cdf06bb..696f1f5c 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -20,19 +20,18 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Forms de l'application cotisation de re2o. Dépendance avec les models, -importé par les views. +Forms for the 'cotisation' app of re2o. It highly depends on +:cotisations:models and is mainly used by :cotisations:views. -Permet de créer une nouvelle facture pour un user (NewFactureForm), -et de l'editer (soit l'user avec EditFactureForm, -soit le trésorier avec TrezEdit qui a plus de possibilités que self -notamment sur le controle trésorier SelectArticleForm est utilisée -lors de la creation d'une facture en -parrallèle de NewFacture pour le choix des articles désirés. -(la vue correspondante est unique) +The following forms are mainly used to create, edit or delete +anything related to 'cotisations' : + * Payments Methods + * Banks + * Invoices + * Articles -ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter, -éditer ou supprimer une banque/moyen de paiement ou un article +See the details for each of these operations in the documentation +of each of the method. """ from __future__ import unicode_literals @@ -51,9 +50,10 @@ from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin class NewFactureForm(FormRevMixin, ModelForm): - """Creation d'une facture, moyen de paiement, banque et numero - de cheque""" - # TODO : translate doc string in English + """ + Form used to create a new invoice by using a payment method, a bank and a + cheque number. + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs) @@ -91,8 +91,10 @@ class NewFactureForm(FormRevMixin, ModelForm): 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): model = Facture fields = ['paiement', 'banque', 'cheque'] @@ -108,8 +110,9 @@ class CreditSoldeForm(NewFactureForm): 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( queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Adherent')), label=_l("Article"), @@ -123,8 +126,9 @@ class SelectUserArticleForm(FormRevMixin, 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( queryset=Article.objects.filter(Q(type_user='All') | Q(type_user='Club')), label=_l("Article"), @@ -138,8 +142,9 @@ class SelectClubArticleForm(Form): # TODO : change Facture to Invoice 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( queryset=Article.objects.all(), label=_l("Article") @@ -162,8 +167,10 @@ class NewFactureFormPdf(Form): # TODO : change Facture to Invoice 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): # TODO : change Facture to Invoice model = Facture @@ -179,8 +186,9 @@ class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): 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: model = Article fields = '__all__' @@ -192,9 +200,10 @@ class ArticleForm(FormRevMixin, ModelForm): class DelArticleForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs articles en vente. Choix - parmis les modèles""" - # TODO : translate docstring to English + """ + Form used to delete one or more of the currently available articles. + The user must choose the one to delete by checking the boxes. + """ articles = forms.ModelMultipleChoiceField( queryset=Article.objects.none(), label=_l("Existing articles"), @@ -212,9 +221,11 @@ class DelArticleForm(FormRevMixin, Form): # TODO : change Paiement to Payment class PaiementForm(FormRevMixin, ModelForm): - """Creation d'un moyen de paiement, champ text moyen et type - permettant d'indiquer si il s'agit d'un chèque ou non pour le form""" - # TODO : translate docstring to English + """ + Form used to create a new payment method. + The 'cheque' type is used to associate a specific behaviour requiring + a cheque number and a bank. + """ class Meta: model = Paiement # TODO : change moyen to method and type_paiement to payment_type @@ -233,9 +244,10 @@ class PaiementForm(FormRevMixin, ModelForm): # TODO : change paiement to payment class DelPaiementForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs moyens de paiements, selection - parmis les models""" - # TODO : translate docstring to English + """ + Form used to delete one or more payment methods. + The user must choose the one to delete by checking the boxes. + """ # TODO : change paiement to payment paiements = forms.ModelMultipleChoiceField( queryset=Paiement.objects.none(), @@ -254,8 +266,9 @@ class DelPaiementForm(FormRevMixin, Form): # TODO : change banque to bank class BanqueForm(FormRevMixin, ModelForm): - """Creation d'une banque, field name""" - # TODO : translate docstring to Englishh + """ + Form used to create a bank. + """ class Meta: # TODO : change banque to bank model = Banque @@ -269,8 +282,10 @@ class BanqueForm(FormRevMixin, ModelForm): # TODO : change banque to bank 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 banques = forms.ModelMultipleChoiceField( queryset=Banque.objects.none(), @@ -289,9 +304,9 @@ class DelBanqueForm(FormRevMixin, Form): # TODO : change facture to Invoice class NewFactureSoldeForm(NewFactureForm): - """Creation d'une facture, moyen de paiement, banque et numero - de cheque""" - # TODO : translate docstring to English + """ + Form used to create an invoice + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) self.fields['cheque'].required = False @@ -335,6 +350,9 @@ class NewFactureSoldeForm(NewFactureForm): # TODO : Better name and docstring class RechargeForm(FormRevMixin, Form): + """ + Form used to refill a user's balance + """ value = forms.FloatField( label=_l("Amount"), min_value=0.01, diff --git a/cotisations/models.py b/cotisations/models.py index d270857f..3edc7815 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -21,28 +21,14 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Definition des models bdd pour les factures et cotisation. -Pièce maitresse : l'ensemble du code intelligent se trouve ici, -dans les clean et save des models ainsi que de leur methodes supplémentaires. +The database models for the 'cotisation' app of re2o. +The goal is to keep the main actions here, i.e. the 'clean' and 'save' +function are higly reposnsible for the changes, checking the coherence of the +data and the good behaviour in general for not breaking the database. -Facture : reliée à un user, elle a un moyen de paiement, une banque (option), -une ou plusieurs ventes - -Article : liste des articles en vente, leur prix, etc - -Vente : ensemble des ventes effectuées, reliées à une facture (foreignkey) - -Banque : liste des banques existantes - -Cotisation : objets de cotisation, contenant un début et une fin. Reliées -aux ventes, en onetoone entre une vente et une cotisation. -Crées automatiquement au save des ventes. - -Post_save et Post_delete : sychronisation des services et régénération -des services d'accès réseau (ex dhcp) lors de la vente d'une cotisation -par exemple +For further details on each of those models, see the documentation details for +each. """ -# TODO : translate docstring to English from __future__ import unicode_literals from dateutil.relativedelta import relativedelta @@ -65,11 +51,24 @@ from re2o.mixins import AclMixin, RevMixin # TODO : change facture to invoice class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): - """ Définition du modèle des factures. Une facture regroupe une ou - plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement - et si il y a lieu un numero pour les chèques. Possède les valeurs - valides et controle (trésorerie)""" - # TODO : translate docstrign to English + """ + The model for an invoice. It reprensents the fact that a user paid for + something (it can be multiple article paid at once). + + An invoice is linked to : + * one or more purchases (one for each article sold that time) + * a user (the one who bought those articles) + * a payment method (the one used by the user) + * (if applicable) a bank + * (if applicable) a cheque number. + Every invoice is dated throught the 'date' value. + An invoice has a 'controlled' value (default : False) which means that + someone with high enough rights has controlled that invoice and taken it + into account. It also has a 'valid' value (default : True) which means + that someone with high enough rights has decided that this invoice was not + valid (thus it's like the user never paid for his articles). It may be + necessary in case of non-payment. + """ user = models.ForeignKey('users.User', on_delete=models.PROTECT) # TODO : change paiement to payment @@ -122,20 +121,21 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): # TODO : change prix to price def prix(self): - """Renvoie le prix brut sans les quantités. Méthode - dépréciée""" - # TODO : translate docstring to English - # TODO : change prix to price - prix = Vente.objects.filter( + """ + Returns: the raw price without the quantities. + Deprecated, use :total_price instead. + """ + price = Vente.objects.filter( facture=self ).aggregate(models.Sum('prix'))['prix__sum'] - return prix + return price # TODO : change prix to price def prix_total(self): - """Prix total : somme des produits prix_unitaire et quantité des - ventes de l'objet""" - # TODO : translate docstrign to English + """ + Returns: the total price for an invoice. Sum all the articles' prices + and take the quantities into account. + """ # TODO : change Vente to somethingelse return Vente.objects.filter( facture=self @@ -147,8 +147,10 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): )['total'] 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( facture=self ).values_list('name', flat=True)) @@ -204,8 +206,9 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @receiver(post_save, sender=Facture) def facture_post_save(sender, **kwargs): - """Post save d'une facture, synchronise l'user ldap""" - # TODO : translate docstrign into English + """ + Synchronise the LDAP user after an invoice has been saved. + """ facture = kwargs['instance'] user = facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -213,18 +216,26 @@ def facture_post_save(sender, **kwargs): @receiver(post_delete, sender=Facture) def facture_post_delete(sender, **kwargs): - """Après la suppression d'une facture, on synchronise l'user ldap""" - # TODO : translate docstring into English + """ + Synchronise the LDAP user after an invoice has been deleted. + """ user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) # TODO : change Vente to Purchase class Vente(RevMixin, AclMixin, models.Model): - """Objet vente, contient une quantité, une facture parente, un nom, - un prix. Peut-être relié à un objet cotisation, via le boolean - iscotisation""" - # TODO : translate docstring into English + """ + The model defining a purchase. It consist of one type of article being + sold. In particular there may be multiple purchases in a single invoice. + + It's reprensentated by: + * an amount (the number of items sold) + * an invoice (whose the purchase is part of) + * an article + * (if applicable) a cotisation (which holds some informations about + the effect of the purchase on the time agreed for this user) + """ # TODO : change this to English COTISATION_TYPE = ( @@ -281,15 +292,16 @@ class Vente(RevMixin, AclMixin, models.Model): # TODO : change prix_total to total_price 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 def update_cotisation(self): - """Mets à jour l'objet related cotisation de la vente, si - il existe : update la date de fin à partir de la durée de - la vente""" - # TODO : translate docstring to English + """ + Update the related object 'cotisation' if there is one. Based on the + duration of the purchase. + """ if hasattr(self, 'cotisation'): cotisation = self.cotisation cotisation.date_end = cotisation.date_start + relativedelta( @@ -297,10 +309,11 @@ class Vente(RevMixin, AclMixin, models.Model): return def create_cotis(self, date_start=False): - """Update et crée l'objet cotisation associé à une facture, prend - en argument l'user, la facture pour la quantitéi, et l'article pour - la durée""" - # TODO : translate docstring to English + """ + Update and create a 'cotisation' related object if there is a + cotisation_type defined (which means the article sold represents + a cotisation) + """ if not hasattr(self, 'cotisation') and self.type_cotisation: cotisation = Cotisation(vente=self) cotisation.type_cotisation = self.type_cotisation @@ -328,8 +341,12 @@ class Vente(RevMixin, AclMixin, models.Model): return 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: raise ValidationError( _("A cotisation should always have a duration.") @@ -372,38 +389,44 @@ class Vente(RevMixin, AclMixin, models.Model): # TODO : change vente to purchase @receiver(post_save, sender=Vente) def vente_post_save(sender, **kwargs): - """Post save d'une vente, déclencge la création de l'objet cotisation - si il y a lieu(si iscotisation) """ - # TODO : translate docstring to English - # TODO : change vente to purchase - vente = kwargs['instance'] + """ + Creates a 'cotisation' related object if needed and synchronise the + LDAP user when a purchase has been saved. + """ + purchase = kwargs['instance'] if hasattr(vente, 'cotisation'): - vente.cotisation.vente = vente - vente.cotisation.save() - if vente.type_cotisation: - vente.create_cotis() - vente.cotisation.save() - user = vente.facture.user + purchase.cotisation.vente = purchase + purchase.cotisation.save() + if purchase.type_cotisation: + purchase.create_cotis() + purchase.cotisation.save() + user = purchase.facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) # TODO : change vente to purchase @receiver(post_delete, sender=Vente) def vente_post_delete(sender, **kwargs): - """Après suppression d'une vente, on synchronise l'user ldap (ex - suppression d'une cotisation""" - # TODO : translate docstring to English - # TODO : change vente to purchase - vente = kwargs['instance'] - if vente.type_cotisation: - user = vente.facture.user + """ + Synchronise the LDAP user after a purchase has been deleted. + """ + purchase = kwargs['instance'] + if purchase.type_cotisation: + user = purchase.facture.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) class Article(RevMixin, AclMixin, models.Model): - """Liste des articles en vente : prix, nom, et attribut iscotisation - et duree si c'est une cotisation""" - # TODO : translate docstring to English + """ + The definition of an article model. It represents an type of object that can be sold to the user. + + It's represented by: + * a name + * a price + * a cotisation type (indicating if this article reprensents a cotisation or not) + * a duration (if it is a cotisation) + * a type of user (indicating what kind of user can buy this article) + """ # TODO : Either use TYPE or TYPES in both choices but not both USER_TYPES = ( @@ -473,8 +496,13 @@ class Article(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( max_length=255, @@ -494,8 +522,14 @@ class Banque(RevMixin, AclMixin, models.Model): # TODO : change Paiement to Payment 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 = ( (0, _l("Standard")), @@ -524,11 +558,16 @@ class Paiement(RevMixin, AclMixin, models.Model): return self.moyen def clean(self): + """ + Override of the herited clean function to get a correct name + """ self.moyen = self.moyen.title() def save(self, *args, **kwargs): - """Un seul type de paiement peut-etre cheque...""" - # 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: raise ValidationError( _("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): - """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 = ( ('Connexion', _l("Connexion")), @@ -602,8 +649,10 @@ class Cotisation(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Cotisation) def cotisation_post_save(sender, **kwargs): - """Après modification d'une cotisation, regeneration des services""" - # 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('dhcp') regen('mac_ip_list') @@ -613,8 +662,10 @@ def cotisation_post_save(sender, **kwargs): # TODO : should be name cotisation_post_delete @receiver(post_delete, sender=Cotisation) def vente_post_delete(sender, **kwargs): - """Après suppression d'une vente, régénération des services""" - # 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'] regen('mac_ip_list') regen('mailing') diff --git a/cotisations/payment.py b/cotisations/payment.py index a83660af..0412ca94 100644 --- a/cotisations/payment.py +++ b/cotisations/payment.py @@ -20,6 +20,9 @@ from .payment_utils.comnpay import Payment as ComnpayPayment @csrf_exempt @login_required def accept_payment(request, factureid): + """ + The view called when an online payment has been accepted. + """ facture = get_object_or_404(Facture, id=factureid) messages.success( request, @@ -33,6 +36,9 @@ def accept_payment(request, factureid): @csrf_exempt @login_required def refuse_payment(request): + """ + The view called when an online payment has been refused. + """ messages.error( request, _("The payment has been refused.") @@ -41,6 +47,11 @@ def refuse_payment(request): @csrf_exempt def ipn(request): + """ + The view called by Comnpay server to validate the transaction. + Verify that we can firmly save the user's action and notify + Comnpay with 400 response if not or with a 200 response if yes + """ p = ComnpayPayment() order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', ) try: @@ -55,8 +66,7 @@ def ipn(request): idTpe = request.POST['idTpe'] idTransaction = request.POST['idTransaction'] - # On vérifie que le paiement nous est destiné - # TODO : translate comment to English + # Checking that the payment is actually for us. if not idTpe == AssoOption.get_cached_value('payment_id'): return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") @@ -67,23 +77,28 @@ def ipn(request): facture = get_object_or_404(Facture, id=factureid) - # TODO : translate comments to English - # On vérifie que le paiement est valide + # Checking that the payment is valid if not result: - # Le paiement a échoué : on effectue les actions nécessaires (On indique qu'elle a échoué) + # Payment failed: Cancelling the invoice operation facture.delete() - - # On notifie au serveur ComNPay qu'on a reçu les données pour traitement + # And send the response to Comnpay indicating we have well + # received the failure information. return HttpResponse("HTTP/1.1 200 OK") facture.valid = True facture.save() - # A nouveau, on notifie au serveur qu'on a bien traité les données + # Everything worked we send a reponse to Comnpay indicating that + # it's ok for them to proceed return HttpResponse("HTTP/1.0 200 OK") def comnpay(facture, request): + """ + Build a request to start the negociation with Comnpay by using + a facture id, the price and the secret transaction data stored in + the preferences. + """ host = request.get_host() p = ComnpayPayment( str(AssoOption.get_cached_value('payment_id')), @@ -110,6 +125,7 @@ def comnpay(facture, request): return r +# The payment systems supported by re2o PAYMENT_SYSTEM = { 'COMNPAY' : comnpay, 'NONE' : None diff --git a/cotisations/tex.py b/cotisations/tex.py index 17d85a13..e3bf62f5 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""tex.py +Module in charge of rendering some LaTex templates. +Used to generated PDF invoice. +""" from django.template.loader import get_template from django.template import Context @@ -37,6 +41,10 @@ CACHE_TIMEOUT = getattr(settings, 'TEX_CACHE_TIMEOUT', 86400) # 1 day def render_invoice(request, ctx={}): + """ + Render an invoice using some available information such as the current + date, the user, the articles, the prices, ... + """ filename = '_'.join([ 'invoice', slugify(ctx['asso_name']), @@ -50,6 +58,11 @@ def render_invoice(request, ctx={}): return r def render_tex(request, template, ctx={}): + """ + Creates a PDF from a LaTex templates using pdflatex. + Writes it in a temporary directory and send back an HTTP response for + accessing this file. + """ context = Context(ctx) template = get_template(template) rendered_tpl = template.render(context).encode('utf-8') diff --git a/cotisations/views.py b/cotisations/views.py index 9c05ade6..f99ff93e 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -73,7 +73,7 @@ from .forms import ( NewFactureSoldeForm, RechargeForm ) -from . import payment +from . import payment as online_payment from .tex import render_invoice @@ -82,50 +82,51 @@ from .tex import render_invoice @can_create(Facture) @can_edit(User) def new_facture(request, user, userid): - """Creation d'une facture pour un user. Renvoie la liste des articles - et crée des factures dans un formset. Utilise un peu de js coté template - pour ajouter des articles. - Parse les article et boucle dans le formset puis save les ventes, - enfin sauve la facture parente. - TODO : simplifier cette fonction, déplacer l'intelligence coté models - Facture et Vente.""" - # TODO : translate docstring to English - # TODO : change facture to invoice - facture = Facture(user=user) - # TODO : change comment to English - # Le template a besoin de connaitre les articles pour le js + """ + View called to create a new invoice. + Currently, Send the list of available articles for the user along with + a formset of a new invoice (based on the `:forms:NewFactureForm()` form. + A bit of JS is used in the template to add articles in a fancier way. + If everything is correct, save each one of the articles, save the + purchase object associated and finally the newly created invoice. + + TODO : The whole verification process should be moved to the model. This + function should only act as a dumb interface between the model and the + user. + """ + invoice = Facture(user=user) + # The template needs the list of articles (for the JS part) article_list = Article.objects.filter( Q(type_user='All') | Q(type_user=request.user.class_name) ) - # On envoie la form fature et un formset d'articles - # TODO : change facture to invoice - facture_form = NewFactureForm(request.POST or None, instance=facture) + # Building the invocie form and the article formset + invoice_form = NewFactureForm(request.POST or None, instance=invoice) if request.user.is_class_club: article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) else: article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) - if facture_form.is_valid() and article_formset.is_valid(): - new_facture_instance = facture_form.save(commit=False) + + if invoice_form.is_valid() and article_formset.is_valid(): + new_invoice_instance = invoice_form.save(commit=False) articles = article_formset - # Si au moins un article est rempli + # Check if at leat one article has been selected if any(art.cleaned_data for art in articles): - # TODO : change solde to balance - user_solde = OptionalUser.get_cached_value('user_solde') - solde_negatif = OptionalUser.get_cached_value('solde_negatif') - # Si on paye par solde, que l'option est activée, - # on vérifie que le négatif n'est pas atteint - if user_solde: + user_balance = OptionalUser.get_cached_value('user_solde') + negative_balance = OptionalUser.get_cached_value('solde_negatif') + # If the paiement using balance has been activated, + # checking that the total price won't get the user under + # the authorized minimum (negative_balance) + if user_balance: # TODO : change Paiement to Payment - if new_facture_instance.paiement == Paiement.objects.get_or_create( + if new_invoice_instance.paiement == Paiement.objects.get_or_create( moyen='solde' )[0]: - prix_total = 0 + total_price = 0 for art_item in articles: if art_item.cleaned_data: - # change prix to price - prix_total += art_item.cleaned_data['article']\ + total_price += art_item.cleaned_data['article']\ .prix*art_item.cleaned_data['quantity'] - if float(user.solde) - float(prix_total) < solde_negatif: + if float(user.solde) - float(total_price) < negative_balance: messages.error( request, _("Your balance is too low for this operation.") @@ -134,20 +135,26 @@ def new_facture(request, user, userid): 'users:profil', kwargs={'userid': userid} )) - new_facture_instance.save() + # Saving the invoice + new_invoice_instance.save() + + # Building a purchase for each article sold for art_item in articles: if art_item.cleaned_data: article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] - new_vente = Vente.objects.create( - facture=new_facture_instance, + new_purchase = Vente.objects.create( + facture=new_invoice_instance, name=article.name, prix=article.prix, type_cotisation=article.type_cotisation, duration=article.duration, number=quantity ) - new_vente.save() + new_purchase.save() + + # In case a cotisation was bought, inform the user, the + # cotisation time has been extended too if any(art_item.cleaned_data['article'].type_cotisation for art_item in articles if art_item.cleaned_data): messages.success( @@ -158,6 +165,7 @@ def new_facture(request, user, userid): end_date: user.end_adhesion() } ) + # Else, only tell the invoice was created else: messages.success( request, @@ -173,7 +181,7 @@ def new_facture(request, user, userid): ) return form( { - 'factureform': facture_form, + 'factureform': invoice_form, 'venteform': article_formset, 'articlelist': article_list }, @@ -185,29 +193,30 @@ def new_facture(request, user, userid): @login_required @can_change(Facture, 'pdf') def new_facture_pdf(request): - """Permet de générer un pdf d'une facture. Réservée - au trésorier, permet d'emettre des factures sans objet - Vente ou Facture correspondant en bdd""" - # TODO : translate docstring to English - facture_form = NewFactureFormPdf(request.POST or None) - if facture_form.is_valid(): + """ + View used to generate a custom PDF invoice. It's mainly used to + get invoices that are not taken into account, for the administrative + point of view. + """ + invoice_form = NewFactureFormPdf(request.POST or None) + if invoice_form.is_valid(): tbl = [] article = facture_form.cleaned_data['article'] - quantite = facture_form.cleaned_data['number'] + quantity = facture_form.cleaned_data['number'] paid = facture_form.cleaned_data['paid'] - destinataire = facture_form.cleaned_data['dest'] - chambre = facture_form.cleaned_data['chambre'] - fid = facture_form.cleaned_data['fid'] + recipient = facture_form.cleaned_data['dest'] + room = facture_form.cleaned_data['chambre'] + invoice_id = facture_form.cleaned_data['fid'] for art in article: - tbl.append([art, quantite, art.prix * quantite]) - prix_total = sum(a[2] for a in tbl) - user = {'name': destinataire, 'room': chambre} + tbl.append([art, quantity, art.prix * quantity]) + total_price = sum(a[2] for a in tbl) + user = {'name': recipient, 'room': room} return render_invoice(request, { 'DATE': timezone.now(), 'dest': user, - 'fid': fid, + 'fid': invoice_id, 'article': tbl, - 'total': prix_total, + 'total': total_price, 'paid': paid, 'asso_name': AssoOption.get_cached_value('name'), 'line1': AssoOption.get_cached_value('adresse1'), @@ -218,8 +227,8 @@ def new_facture_pdf(request): 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) }) return form({ - 'factureform': facture_form, - 'action_name' : 'Editer' + 'factureform': invoice_form, + 'action_name': _("Edit") }, 'cotisations/facture.html', request) @@ -227,22 +236,23 @@ def new_facture_pdf(request): @login_required @can_view(Facture) def facture_pdf(request, facture, factureid): - """Affiche en pdf une facture. Cree une ligne par Vente de la facture, - et génére une facture avec le total, le moyen de paiement, l'adresse - de l'adhérent, etc. Réservée à self pour un user sans droits, - les droits cableurs permettent d'afficher toute facture""" - # TODO : translate docstring to English + """ + View used to generate a PDF file from an existing invoice in database + Creates a line for each Purchase (thus article sold) and generate the + invoice with the total price, the payment method, the address and the + legal information for the user. + """ # TODO : change vente to purchase - ventes_objects = Vente.objects.all().filter(facture=facture) - ventes = [] - for vente in ventes_objects: - ventes.append([vente, vente.number, vente.prix_total]) + purchases_objects = Vente.objects.all().filter(facture=facture) + purchases = [] + for purchase in purchases_objects: + purchases.append([purchase, purchase.number, purchase.prix_total]) return render_invoice(request, { 'paid': True, 'fid': facture.id, 'DATE': facture.date, 'dest': facture.user, - 'article': ventes, + 'article': purchases, 'total': facture.prix_total(), 'asso_name': AssoOption.get_cached_value('name'), 'line1': AssoOption.get_cached_value('adresse1'), @@ -258,31 +268,33 @@ def facture_pdf(request, facture, factureid): @login_required @can_edit(Facture) def edit_facture(request, facture, factureid): - """Permet l'édition d'une facture. On peut y éditer les ventes - déjà effectuer, ou rendre une facture invalide (non payées, chèque - en bois etc). Mets à jour les durée de cotisation attenantes""" - # TODO : translate docstring to English - facture_form = EditFactureForm(request.POST or None, instance=facture, user=request.user) - ventes_objects = Vente.objects.filter(facture=facture) - vente_form_set = modelformset_factory( + """ + View used to edit an existing invoice. + Articles can be added or remove to the invoice and quantity + can be set as desired. This is also the view used to invalidate + an invoice. + """ + invoice_form = EditFactureForm(request.POST or None, instance=facture, user=request.user) + purchases_objects = Vente.objects.filter(facture=facture) + purchase_form_set = modelformset_factory( Vente, fields=('name', 'number'), extra=0, - max_num=len(ventes_objects) + max_num=len(purchases_objects) ) - 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() + 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.") ) return redirect(reverse('cotisations:index')) return form({ - 'factureform': facture_form, - 'venteform': vente_form + 'factureform': invoice_form, + 'venteform': purchase_form }, 'cotisations/edit_facture.html', request) @@ -290,10 +302,11 @@ def edit_facture(request, facture, factureid): @login_required @can_delete(Facture) def del_facture(request, facture, factureid): - """Suppression d'une facture. Supprime en cascade les ventes - et cotisations filles""" - # TODO : translate docstring to English + """ + View used to delete an existing invocie. + """ if request.method == "POST": + facture.delete() messages.success( request, _("The invoice has been successfully deleted.") @@ -301,7 +314,7 @@ def del_facture(request, facture, factureid): return redirect(reverse('cotisations:index')) return form({ 'objet': facture, - 'objet_name': 'facture' + 'objet_name': _("Invoice") }, 'cotisations/delete.html', request) @@ -310,40 +323,46 @@ def del_facture(request, facture, factureid): @can_create(Facture) @can_edit(User) 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 - facture = CreditSoldeForm(request.POST or None) - if facture.is_valid(): - facture_instance = facture.save(commit=False) - facture_instance.user = user - facture_instance.save() - new_vente = Vente.objects.create( - facture=facture_instance, + invoice = CreditSoldeForm(request.POST or None) + if invoice.is_valid(): + invoice_instance = invoice.save(commit=False) + invoice_instance.user = user + invoice_instance.save() + new_purchase = Vente.objects.create( + facture=invoice_instance, name="solde", - prix=facture.cleaned_data['montant'], + prix=invoice.cleaned_data['montant'], number=1 ) - new_vente.save() + new_purchase.save() messages.success( request, _("Balance successfully updated.") ) return redirect(reverse('cotisations:index')) - return form({'factureform': facture, 'action_name' : 'Créditer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': facture, + 'action_name': _("Edit") + }, 'cotisations/facture.html', request) @login_required @can_create(Article) def add_article(request): - """Ajoute un article. Champs : désignation, - prix, est-ce une cotisation et si oui sa durée - Réservé au trésorier - Nota bene : les ventes déjà effectuées ne sont pas reliées - aux articles en vente. La désignation, le prix... sont - copiés à la création de la facture. Un changement de prix n'a - PAS de conséquence sur les ventes déjà faites""" - # TODO : translate docstring to English + """ + View used to add an article. + + .. note:: If a purchase has already been sold, the price are calculated + once and for all. That means even if the price of an article is edited + later, it won't change the invoice. That is really important to keep + this behaviour in order not to modify all the past and already + accepted invoices. + """ article = ArticleForm(request.POST or None) if article.is_valid(): article.save() @@ -352,15 +371,18 @@ def add_article(request): _("The article has been successfully created.") ) return redirect(reverse('cotisations:index-article')) - return form({'factureform': article, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) + return form({ + 'factureform': article, + 'action_name': _("Add") + }, 'cotisations/facture.html', request) @login_required @can_edit(Article) def edit_article(request, article_instance, articleid): - """Edition d'un article (designation, prix, etc) - Réservé au trésorier""" - # TODO : translate dosctring to English + """ + View used to edit an article. + """ article = ArticleForm(request.POST or None, instance=article_instance) if article.is_valid(): if article.changed_data: @@ -370,14 +392,18 @@ def edit_article(request, article_instance, articleid): _("The article has been successfully edited.") ) return redirect(reverse('cotisations:index-article')) - return form({'factureform': article, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': article, + 'action_name': _('Edit') + }, 'cotisations/facture.html', request) @login_required @can_delete_set(Article) def del_article(request, instances): - """Suppression d'un article en vente""" - # TODO : translate docstring to English + """ + View used to delete one of the articles. + """ article = DelArticleForm(request.POST or None, instances=instances) if article.is_valid(): article_del = article.cleaned_data['articles'] @@ -387,63 +413,73 @@ def del_article(request, instances): _("The article(s) have been successfully deleted.") ) return redirect(reverse('cotisations:index-article')) - return form({'factureform': article, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': article, + 'action_name': _("Delete") + }, 'cotisations/facture.html', request) # TODO : change paiement to payment @login_required @can_create(Paiement) def add_paiement(request): - """Ajoute un moyen de paiement. Relié aux factures - via foreign key""" - # TODO : translate docstring to English - # TODO : change paiement to Payment - paiement = PaiementForm(request.POST or None) - if paiement.is_valid(): - paiement.save() + """ + View used to add a payment method. + """ + payment = PaiementForm(request.POST or None) + if payment.is_valid(): + payment.save() messages.success( request, _("The payment method has been successfully created.") ) return redirect(reverse('cotisations:index-paiement')) - return form({'factureform': paiement, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) + return form({ + 'factureform': payment, + 'action_name': _("Add") + }, 'cotisations/facture.html', request) # TODO : chnage paiement to Payment @login_required @can_edit(Paiement) def edit_paiement(request, paiement_instance, paiementid): - """Edition d'un moyen de paiement""" - # TODO : translate docstring to English - paiement = PaiementForm(request.POST or None, instance=paiement_instance) - if paiement.is_valid(): - if paiement.changed_data: - paiement.save() + """ + View used to edit a payment method. + """ + payment = PaiementForm(request.POST or None, instance=paiement_instance) + if payment.is_valid(): + if payment.changed_data: + payment.save() messages.success( request, _("The payement method has been successfully edited.") ) return redirect(reverse('cotisations:index-paiement')) - return form({'factureform': paiement, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': payment, + 'action_name': _("Edit") + }, 'cotisations/facture.html', request) # TODO : change paiement to payment @login_required @can_delete_set(Paiement) def del_paiement(request, instances): - """Suppression d'un moyen de paiement""" - # TODO : translate docstring to English - paiement = DelPaiementForm(request.POST or None, instances=instances) - if paiement.is_valid(): - paiement_dels = paiement.cleaned_data['paiements'] - for paiement_del in paiement_dels: + """ + View used to delete a set of payment methods. + """ + payment = DelPaiementForm(request.POST or None, instances=instances) + if payment.is_valid(): + payment_dels = payment.cleaned_data['paiements'] + for payment_del in payment_dels: try: - paiement_del.delete() + payment_del.delete() messages.success( request, _("The payment method %(method_name)s has been \ successfully deleted.") % { - method_name: paiement_del + method_name: payment_del } ) except ProtectedError: @@ -451,65 +487,77 @@ def del_paiement(request, instances): request, _("The payment method %(method_name)s can't be deleted \ because there are invoices using it.") % { - method_name: paiement_del + method_name: payment_del } ) return redirect(reverse('cotisations:index-paiement')) - return form({'factureform': paiement, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': payment, + 'action_name': _("Delete") + }, 'cotisations/facture.html', request) # TODO : change banque to bank @login_required @can_create(Banque) def add_banque(request): - """Ajoute une banque à la liste des banques""" - # TODO : tranlate docstring to English - banque = BanqueForm(request.POST or None) - if banque.is_valid(): - banque.save() + """ + View used to add a bank. + """ + bank = BanqueForm(request.POST or None) + if bank.is_valid(): + bank.save() messages.success( request, _("The bank has been successfully created.") ) return redirect(reverse('cotisations:index-banque')) - return form({'factureform': banque, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) + return form({ + 'factureform': bank, + 'action_name': _("Add") + }, 'cotisations/facture.html', request) # TODO : change banque to bank @login_required @can_edit(Banque) def edit_banque(request, banque_instance, banqueid): - """Edite le nom d'une banque""" - # TODO : translate docstring to English - banque = BanqueForm(request.POST or None, instance=banque_instance) - if banque.is_valid(): - if banque.changed_data: - banque.save() + """ + View used to edit a bank. + """ + bank = BanqueForm(request.POST or None, instance=banque_instance) + if bank.is_valid(): + if bank.changed_data: + bank.save() messages.success( request, _("The bank has been successfully edited") ) return redirect(reverse('cotisations:index-banque')) - return form({'factureform': banque, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': bank, + 'action_name': _("Edit") + }, 'cotisations/facture.html', request) # TODO : chnage banque to bank @login_required @can_delete_set(Banque) def del_banque(request, instances): - """Supprime une banque""" - # TODO : translate docstring to English - banque = DelBanqueForm(request.POST or None, instances=instances) - if banque.is_valid(): - banque_dels = banque.cleaned_data['banques'] - for banque_del in banque_dels: + """ + View used to delete a set of banks. + """ + bank = DelBanqueForm(request.POST or None, instances=instances) + if bank.is_valid(): + bank_dels = bank.cleaned_data['banques'] + for bank_del in bank_dels: try: - banque_del.delete() + bank_del.delete() messages.success( request, _("The bank %(bank_name)s has been successfully \ deleted.") % { - bank_name: banque_del + bank_name: bank_del } ) except ProtectedError: @@ -517,11 +565,14 @@ def del_banque(request, instances): request, _("The bank %(bank_name)s can't be deleted \ because there are invoices using it.") % { - bank_name: banque_del + bank_name: bank_del } ) return redirect(reverse('cotisations:index-banque')) - return form({'factureform': banque, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) + return form({ + 'factureform': bank, + 'action_name': _("Delete") + }, 'cotisations/facture.html', request) # TODO : change facture to invoice @@ -529,39 +580,44 @@ def del_banque(request, instances): @can_view_all(Facture) @can_change(Facture, 'control') def control(request): - """Pour le trésorier, vue pour controler en masse les - factures.Case à cocher, pratique""" - # TODO : translate docstring to English + """ + View used to control the invoices all at once. + """ pagination_number = GeneralOption.get_cached_value('pagination_number') - facture_list = Facture.objects.select_related('user').select_related('paiement') - facture_list = SortTable.sort( - facture_list, + invoice_list = Facture.objects.select_related('user').select_related('paiement') + invoice_list = SortTable.sort( + invoice_list, request.GET.get('col'), request.GET.get('order'), SortTable.COTISATIONS_CONTROL ) - controlform_set = modelformset_factory( + control_invoices_formset = modelformset_factory( Facture, fields=('control', 'valid'), extra=0 - ) - facture_list = re2o_paginator(request, facture_list, pagination_number) - controlform = controlform_set(request.POST or None, queryset=facture_list.object_list) - if controlform.is_valid(): - controlform.save() + ) + invoice_list = re2o_paginator(request, invoice_list, pagination_number) + control_invoices_form = control_invoices_formset( + request.POST or None, + queryset=invoice_list.object_list + ) + if control_invoices_form.is_valid(): + control_invoices_form.save() reversion.set_comment("Controle") return redirect(reverse('cotisations:control')) return render(request, 'cotisations/control.html', { - 'facture_list': facture_list, - 'controlform': controlform + 'facture_list': invoice_list, + 'controlform': control_invoices_form }) @login_required @can_view_all(Article) def index_article(request): - """Affiche l'ensemble des articles en vente""" - # 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') return render(request, 'cotisations/index_article.html', { 'article_list': article_list @@ -572,11 +628,12 @@ def index_article(request): @login_required @can_view_all(Paiement) def index_paiement(request): - """Affiche l'ensemble des moyens de paiement en vente""" - # TODO : translate docstring to English - paiement_list = Paiement.objects.order_by('moyen') + """ + View used to display the list of all available payment methods. + """ + payment_list = Paiement.objects.order_by('moyen') return render(request, 'cotisations/index_paiement.html', { - 'paiement_list': paiement_list + 'paiement_list': payment_list }) @@ -584,51 +641,57 @@ def index_paiement(request): @login_required @can_view_all(Banque) def index_banque(request): - """Affiche l'ensemble des banques""" - # TODO : translate docstring to English - banque_list = Banque.objects.order_by('name') + """ + View used to display the list of all available banks. + """ + bank_list = Banque.objects.order_by('name') return render(request, 'cotisations/index_banque.html', { - 'banque_list': banque_list + 'banque_list': bank_list }) @login_required @can_view_all(Facture) def index(request): - """Affiche l'ensemble des factures, pour les cableurs et +""" - # TODO : translate docstring to English + """ + View used to display the list of all exisitng invoices. + """ pagination_number = GeneralOption.get_cached_value('pagination_number') - facture_list = Facture.objects.select_related('user')\ + invoice_list = Facture.objects.select_related('user')\ .select_related('paiement').prefetch_related('vente_set') - facture_list = SortTable.sort( - facture_list, + invoice_list = SortTable.sort( + invoice_list, request.GET.get('col'), request.GET.get('order'), SortTable.COTISATIONS_INDEX ) - facture_list = re2o_paginator(request, facture_list, pagination_number) + invoice_list = re2o_paginator(request, invoice_list, pagination_number) return render(request, 'cotisations/index.html', { - 'facture_list': facture_list + 'facture_list': invoice_list }) +# TODO : merge this function with new_facture() which is nearly the same # TODO : change facture to invoice @login_required def new_facture_solde(request, userid): - """Creation d'une facture pour un user. Renvoie la liste des articles - et crée des factures dans un formset. Utilise un peu de js coté template - pour ajouter des articles. - Parse les article et boucle dans le formset puis save les ventes, - enfin sauve la facture parente. - TODO : simplifier cette fonction, déplacer l'intelligence coté models - Facture et Vente.""" - # TODO : translate docstring to English + """ + View called to create a new invoice when using the balance to pay. + Currently, send the list of available articles for the user along with + a formset of a new invoice (based on the `:forms:NewFactureForm()` form. + A bit of JS is used in the template to add articles in a fancier way. + If everything is correct, save each one of the articles, save the + purchase object associated and finally the newly created invoice. + + TODO : The whole verification process should be moved to the model. This + function should only act as a dumb interface between the model and the + user. + """ user = request.user - facture = Facture(user=user) - paiement, _created = Paiement.objects.get_or_create(moyen='Solde') - facture.paiement = paiement - # TODO : translate comments to English - # Le template a besoin de connaitre les articles pour le js + invoice = Facture(user=user) + payment, _created = Paiement.objects.get_or_create(moyen='Solde') + facture.paiement = payment + # The template needs the list of articles (for the JS part) article_list = Article.objects.filter( Q(type_user='All') | Q(type_user=request.user.class_name) ) @@ -636,21 +699,23 @@ def new_facture_solde(request, userid): article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) else: article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) + if article_formset.is_valid(): articles = article_formset - # Si au moins un article est rempli + # Check if at leat one article has been selected if any(art.cleaned_data for art in articles): - user_solde = OptionalUser.get_cached_value('user_solde') - solde_negatif = OptionalUser.get_cached_value('solde_negatif') - # Si on paye par solde, que l'option est activée, - # on vérifie que le négatif n'est pas atteint - if user_solde: - prix_total = 0 + user_balance = OptionalUser.get_cached_value('user_solde') + negative_balance = OptionalUser.get_cached_value('solde_negatif') + # If the paiement using balance has been activated, + # checking that the total price won't get the user under + # the authorized minimum (negative_balance) + if user_balance: + total_price = 0 for art_item in articles: if art_item.cleaned_data: - prix_total += art_item.cleaned_data['article']\ + total_price += art_item.cleaned_data['article']\ .prix*art_item.cleaned_data['quantity'] - if float(user.solde) - float(prix_total) < solde_negatif: + if float(user.solde) - float(total_price) < negative_balance: messages.error( request, _("The balance is too low for this operation.") @@ -659,20 +724,26 @@ def new_facture_solde(request, userid): 'users:profil', kwargs={'userid': userid} )) - facture.save() + # Saving the invoice + invoice.save() + + # Building a purchase for each article sold for art_item in articles: if art_item.cleaned_data: article = art_item.cleaned_data['article'] quantity = art_item.cleaned_data['quantity'] - new_vente = Vente.objects.create( - facture=facture, + new_purchase = Vente.objects.create( + facture=invoice, name=article.name, prix=article.prix, type_cotisation=article.type_cotisation, duration=article.duration, number=quantity ) - new_vente.save() + new_purchase.save() + + # In case a cotisation was bought, inform the user, the + # cotisation time has been extended too if any(art_item.cleaned_data['article'].type_cotisation for art_item in articles if art_item.cleaned_data): messages.success( @@ -683,6 +754,7 @@ def new_facture_solde(request, userid): end_date: user.end_adhesion() } ) + # Else, only tell the invoice was created else: messages.success( request, @@ -707,9 +779,12 @@ def new_facture_solde(request, userid): }, 'cotisations/new_facture_solde.html', request) -# TODO : change recharge to reload +# TODO : change recharge to refill @login_required def recharge(request): + """ + View used to refill the balance by using online payment. + """ if AssoOption.get_cached_value('payment') == 'NONE': messages.error( request, @@ -719,20 +794,22 @@ def recharge(request): 'users:profil', kwargs={'userid': request.user.id} )) - f = RechargeForm(request.POST or None, user=request.user) - if f.is_valid(): - facture = Facture(user=request.user) - paiement, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne') - facture.paiement = paiement + refill_form = RechargeForm(request.POST or None, user=request.user) + if refill_form.is_valid(): + invoice = Facture(user=request.user) + payment, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne') + facture.paiement = payment facture.valid = False facture.save() - v = Vente.objects.create( - facture=facture, + purchase = Vente.objects.create( + facture=invoice, name='solde', - prix=f.cleaned_data['value'], - number=1, + prix=refill_form.cleaned_data['value'], + number=1 ) - v.save() - content = payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](facture, request) + purchase.save() + content = online_payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](invoice, request) return render(request, 'cotisations/payment.html', content) - return form({'rechargeform':f}, 'cotisations/recharge.html', request) + return form({ + 'rechargeform': refill_form + }, 'cotisations/recharge.html', request)