mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-25 22:22:26 +00:00
Translation : translate docstrings of cotisations
This commit is contained in:
parent
8da337c549
commit
f2f4336e87
5 changed files with 541 additions and 366 deletions
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue