8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-23 07:23:46 +00:00

Merge branch 'fix_93_store_custom_invoices' into 'dev'

Fix 93 store custom invoices

See merge request federez/re2o!203
This commit is contained in:
klafyvel 2018-08-16 00:39:09 +02:00
commit 3cae0564ef
16 changed files with 891 additions and 447 deletions

View file

@ -136,3 +136,17 @@ Fix several issues with email accounts, you need to collect the static files.
```bash ```bash
./manage.py collectstatic ./manage.py collectstatic
``` ```
## MR 203 Add custom invoices
The custom invoices are now stored in database. You need to migrate your database :
```bash
python3 manage.py migrate
```
On some database engines (postgreSQL) you also need to update the id sequences:
```bash
python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell
```

View file

@ -30,6 +30,7 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
from .models import CustomInvoice
class FactureAdmin(VersionAdmin): class FactureAdmin(VersionAdmin):
@ -37,6 +38,11 @@ class FactureAdmin(VersionAdmin):
pass pass
class CustomInvoiceAdmin(VersionAdmin):
"""Admin class for custom invoices."""
pass
class VenteAdmin(VersionAdmin): class VenteAdmin(VersionAdmin):
"""Class admin d'une vente, tous les champs (facture related)""" """Class admin d'une vente, tous les champs (facture related)"""
pass pass
@ -69,3 +75,4 @@ admin.site.register(Banque, BanqueAdmin)
admin.site.register(Paiement, PaiementAdmin) admin.site.register(Paiement, PaiementAdmin)
admin.site.register(Vente, VenteAdmin) admin.site.register(Vente, VenteAdmin)
admin.site.register(Cotisation, CotisationAdmin) admin.site.register(Cotisation, CotisationAdmin)
admin.site.register(CustomInvoice, CustomInvoiceAdmin)

View file

@ -46,7 +46,7 @@ from django.shortcuts import get_object_or_404
from re2o.field_permissions import FieldPermissionFormMixin from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
from .models import Article, Paiement, Facture, Banque from .models import Article, Paiement, Facture, Banque, CustomInvoice
from .payment_methods import balance from .payment_methods import balance
@ -131,24 +131,13 @@ class SelectClubArticleForm(Form):
self.fields['article'].queryset = Article.find_allowed_articles(user) self.fields['article'].queryset = Article.find_allowed_articles(user)
# TODO : change Facture to Invoice class CustomInvoiceForm(FormRevMixin, ModelForm):
class NewFactureFormPdf(Form):
""" """
Form used to create a custom PDF invoice. Form used to create a custom invoice.
""" """
paid = forms.BooleanField(label=_l("Paid"), required=False) class Meta:
# TODO : change dest field to recipient model = CustomInvoice
dest = forms.CharField( fields = '__all__'
required=True,
max_length=255,
label=_l("Recipient")
)
# TODO : change chambre field to address
chambre = forms.CharField(
required=False,
max_length=10,
label=_l("Address")
)
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):

View file

@ -21,9 +21,9 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2.5\n" "Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-05-10 15:21-0500\n" "POT-Creation-Date: 2018-07-25 23:22+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n" "PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Maël Kervella <dev@maelkervella.eu>\n" "Last-Translator: Hugo Levy-Falk <me@klafyvel.me>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: fr_FR\n" "Language: fr_FR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -34,74 +34,39 @@ msgstr ""
msgid "You don't have the rights to see this application." msgid "You don't have the rights to see this application."
msgstr "Vous n'avez pas les droits de voir cette application." msgstr "Vous n'avez pas les droits de voir cette application."
#: forms.py:63 forms.py:321 #: forms.py:63 forms.py:274
msgid "Cheque number"
msgstr "Numéro de chèque"
#: forms.py:64 forms.py:322
msgid "Not specified"
msgstr "Non renseigné"
#: forms.py:66 forms.py:324
msgid "Select a payment method" msgid "Select a payment method"
msgstr "Sélectionnez un moyen de paiement" msgstr "Sélectionnez un moyen de paiement"
#: forms.py:83 forms.py:347 #: forms.py:66
msgid "A payment method must be specified."
msgstr "Un moyen de paiement doit être renseigné."
#: forms.py:87 forms.py:352
msgid "A cheque number and a bank must be specified."
msgstr "Un numéro de chèqe et une banque doivent être renseignés."
#: forms.py:184
msgid "Member" msgid "Member"
msgstr "Adhérent" msgstr "Adhérent"
#: forms.py:186 #: forms.py:68
msgid "Select the proprietary member" msgid "Select the proprietary member"
msgstr "Sélectionnez l'adhérent propriétaire" msgstr "Sélectionnez l'adhérent propriétaire"
#: forms.py:187 #: forms.py:69
msgid "Validated invoice" msgid "Validated invoice"
msgstr "Facture validée" msgstr "Facture validée"
#: forms.py:201 #: forms.py:82
msgid "A payment method must be specified."
msgstr "Un moyen de paiement doit être renseigné."
#: forms.py:154
msgid "Article name" msgid "Article name"
msgstr "Nom de l'article" msgstr "Nom de l'article"
#: forms.py:239 #: forms.py:192
msgid "Payment method name" msgid "Payment method name"
msgstr "Nom du moyen de paiement" msgstr "Nom du moyen de paiement"
#: forms.py:240 #: forms.py:230
msgid "Payment type"
msgstr "Type de paiement"
#: forms.py:242
msgid ""
"The payement type is used for specific behaviour. The \"cheque\" "
"type means a cheque number and a bank name may be added when "
"using this payment method."
msgstr ""
"Le type de paiement est utilisé pour des comportements spécifiques. Le type "
"\"chèque\" permet de spécifier un numéro de chèque et une banque lors de "
"l'utilisation de cette méthode."
#: forms.py:282
msgid "Bank name" msgid "Bank name"
msgstr "Nom de la banque" msgstr "Nom de la banque"
#: forms.py:380 #: forms.py:287
#, python-format
msgid ""
"Requested amount is too small. Minimum amount possible : "
"%(min_online_amount)s €."
msgstr ""
"Montant demandé est trop faible. Montant minimal possible : "
"%(min_online_amount)s €"
#: forms.py:390
#, python-format #, python-format
msgid "" msgid ""
"Requested amount is too high. Your balance can't exceed " "Requested amount is too high. Your balance can't exceed "
@ -110,15 +75,15 @@ msgstr ""
"Montant demandé trop grand. Votre solde ne peut excéder " "Montant demandé trop grand. Votre solde ne peut excéder "
"%(max_online_balance)s €" "%(max_online_balance)s €"
#: models.py:165 models.py:213 #: models.py:175 models.py:223
msgid "You don't have the right to edit an invoice." msgid "You don't have the right to edit an invoice."
msgstr "Vous n'avez pas le droit de modifier une facture." msgstr "Vous n'avez pas le droit de modifier une facture."
#: models.py:168 #: models.py:178
msgid "You don't have the right to edit this user's invoices." msgid "You don't have the right to edit this user's invoices."
msgstr "Vous n'avez pas le droit de modifier les facture de cette utilisateur." msgstr "Vous n'avez pas le droit de modifier les facture de cette utilisateur."
#: models.py:172 #: models.py:182
msgid "" msgid ""
"You don't have the right to edit an invoice already controlled or " "You don't have the right to edit an invoice already controlled or "
"invalidated." "invalidated."
@ -126,15 +91,15 @@ msgstr ""
"Vous n'avez pas le droit de modifier une facture précedement controllée ou " "Vous n'avez pas le droit de modifier une facture précedement controllée ou "
"invalidée." "invalidée."
#: models.py:179 #: models.py:189
msgid "You don't have the right to delete an invoice." msgid "You don't have the right to delete an invoice."
msgstr "Vous n'avez pas le droit de supprimer une facture." msgstr "Vous n'avez pas le droit de supprimer une facture."
#: models.py:181 #: models.py:191
msgid "You don't have the right to delete this user's invoices." msgid "You don't have the right to delete this user's invoices."
msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur." msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur."
#: models.py:184 #: models.py:194
msgid "" msgid ""
"You don't have the right to delete an invoice already controlled or " "You don't have the right to delete an invoice already controlled or "
"invalidated." "invalidated."
@ -142,35 +107,41 @@ msgstr ""
"Vous n'avez pas le droit de supprimer une facture précedement controllée ou " "Vous n'avez pas le droit de supprimer une facture précedement controllée ou "
"invalidée." "invalidée."
#: models.py:192 #: models.py:202
msgid "You don't have the right to see someone else's invoices history." msgid "You don't have the right to see someone else's invoices history."
msgstr "" msgstr ""
"Vous n'avez pas le droit de voir l'historique de la facture de quelqu'un " "Vous n'avez pas le droit de voir l'historique de la facture de quelqu'un "
"d'autre." "d'autre."
#: models.py:195 #: models.py:205
msgid "The invoice has been invalidated." msgid "The invoice has been invalidated."
msgstr "La facture a été invalidée." msgstr "La facture a été invalidée."
#: models.py:205 #: models.py:215
#, fuzzy
#| msgid "You don't have the right to edit the controlled state."
msgid "You don't have the right to edit the \"controlled\" state." msgid "You don't have the right to edit the \"controlled\" state."
msgstr "Vous n'avez pas le droit de modifier l'état \"controllé\"." msgstr "Vous n'avez pas le droit de modifier l'état \"controllé\"."
#: models.py:372 #: models.py:237
msgid "There are no payment types which you can use."
msgstr "Il n'y a pas de type de paiement que vous puissiez utiliser."
#: models.py:239
msgid "There are no article that you can buy."
msgstr "Il n'y a pas d'article qui vous soit autorisé."
#: models.py:424
msgid "A cotisation should always have a duration." msgid "A cotisation should always have a duration."
msgstr "Une cotisation devrait toujours avoir une durée." msgstr "Une cotisation devrait toujours avoir une durée."
#: models.py:379 #: models.py:431
msgid "You don't have the right to edit the purchases." msgid "You don't have the right to edit the purchases."
msgstr "Vous n'avez pas le droit de modifier les achats." msgstr "Vous n'avez pas le droit de modifier les achats."
#: models.py:384 #: models.py:436
msgid "You don't have the right to edit this user's purchases." msgid "You don't have the right to edit this user's purchases."
msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur." msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur."
#: models.py:388 #: models.py:440
msgid "" msgid ""
"You don't have the right to edit a purchase already controlled or " "You don't have the right to edit a purchase already controlled or "
"invalidated." "invalidated."
@ -178,15 +149,15 @@ msgstr ""
"Vous n'avez pas le droit de modifier un achat précédement controllé ou " "Vous n'avez pas le droit de modifier un achat précédement controllé ou "
"invalidé." "invalidé."
#: models.py:395 #: models.py:447
msgid "You don't have the right to delete a purchase." msgid "You don't have the right to delete a purchase."
msgstr "Vous n'avez pas le droit de supprimer un achat." msgstr "Vous n'avez pas le droit de supprimer un achat."
#: models.py:397 #: models.py:449
msgid "You don't have the right to delete this user's purchases." msgid "You don't have the right to delete this user's purchases."
msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur." msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur."
#: models.py:400 #: models.py:452
msgid "" msgid ""
"You don't have the right to delete a purchase already controlled or " "You don't have the right to delete a purchase already controlled or "
"invalidated." "invalidated."
@ -194,29 +165,46 @@ msgstr ""
"Vous n'avez pas le droit de supprimer un achat précédement controllé ou " "Vous n'avez pas le droit de supprimer un achat précédement controllé ou "
"invalidé." "invalidé."
#: models.py:408 #: models.py:460
msgid "You don't have the right to see someone else's purchase history." msgid "You don't have the right to see someone else's purchase history."
msgstr "" msgstr ""
"Vous n'avez pas le droit de voir l'historique d'un achat de quelqu'un " "Vous n'avez pas le droit de voir l'historique d'un achat de quelqu'un "
"d'autre." "d'autre."
#: models.py:517 #: models.py:582
msgid "Solde is a reserved article name" msgid "Solde is a reserved article name"
msgstr "Solde est un nom d'article réservé" msgstr "Solde est un nom d'article réservé"
#: models.py:521 #: models.py:586
msgid "Duration must be specified for a cotisation" msgid "Duration must be specified for a cotisation"
msgstr "La durée doit être spécifiée pour une cotisation" msgstr "La durée doit être spécifiée pour une cotisation"
#: models.py:603 #: models.py:607
msgid "You cannot have multiple payment method of type cheque" msgid "You cannot buy this Article."
msgstr "Vous ne pouvez avoir plusieurs moyens de paiement de type chèque" msgstr "Vous ne pouvez pas acheter cet article."
#: models.py:654 #: models.py:713 payment_methods/comnpay/views.py:63
msgid ""
"The cotisation of %(member_name)s has been extended to %(end_date)s."
msgstr "La cotisation de %(member_name)s a été étendu jusqu'à %(end_date)s."
#: models.py:723
msgid "The invoice has been created."
msgstr "La facture a été créée."
#: models.py:744
msgid "You cannot use this Payment."
msgstr "Vous ne pouvez pas utiliser ce Paiement."
#: models.py:762
msgid "No custom payment method"
msgstr "Pas de méthode de paiement personnalisée"
#: models.py:811
msgid "You don't have the right to edit a cotisation." msgid "You don't have the right to edit a cotisation."
msgstr "Vous n'avez pas le droit de modifier une cotisation." msgstr "Vous n'avez pas le droit de modifier une cotisation."
#: models.py:658 #: models.py:815
msgid "" msgid ""
"You don't have the right to edit a cotisation already controlled or " "You don't have the right to edit a cotisation already controlled or "
"invalidated." "invalidated."
@ -224,11 +212,11 @@ msgstr ""
"Vous n'avez pas le droit de modifier une cotisaiton précédement controllée " "Vous n'avez pas le droit de modifier une cotisaiton précédement controllée "
"ou invalidée." "ou invalidée."
#: models.py:665 #: models.py:822
msgid "You don't have the right to delete a cotisation." msgid "You don't have the right to delete a cotisation."
msgstr "Vous n'avez pas le droit de supprimer une cotisation." msgstr "Vous n'avez pas le droit de supprimer une cotisation."
#: models.py:668 #: models.py:825
msgid "" msgid ""
"You don't have the right to delete a cotisation already controlled or " "You don't have the right to delete a cotisation already controlled or "
"invalidated." "invalidated."
@ -236,113 +224,159 @@ msgstr ""
"Vous n'avez pas le droit de supprimer une cotisation précédement controllée " "Vous n'avez pas le droit de supprimer une cotisation précédement controllée "
"ou invalidée." "ou invalidée."
#: models.py:676 #: models.py:833
msgid "You don't have the right to see someone else's cotisation history." msgid "You don't have the right to see someone else's cotisation history."
msgstr "" msgstr ""
"Vous n'avez pas le droit de voir l'historique d'une cotisation de quelqu'un " "Vous n'avez pas le droit de voir l'historique d'une cotisation de quelqu'un "
"d'autre." "d'autre."
#: payment.py:31 #: payment_methods/balance/models.py:82 payment_methods/balance/models.py:113
msgid "Your balance is too low for this operation."
msgstr "Votre solde est trop faible pour cette opération."
#: payment_methods/balance/models.py:100
msgid "There is already a payment type for user balance"
msgstr "Il y a déjà un type de paiement pour le solde utilisateur"
#: payment_methods/cheque/views.py:47
msgid "You cannot pay this invoice with a cheque."
msgstr "Vous ne pouvez pas payer cette facture avec un chèque."
#: payment_methods/comnpay/models.py:94
msgid "Pay invoice no : "
msgstr "Payer la facture numéro : "
#: payment_methods/comnpay/models.py:106
msgid ""
"In order to pay your invoice with ComNpay, the price must be grater than {} €"
msgstr ""
"Pour pouvoir payer votre facture avec ComNpay, le prix doit être plus grand "
"que {} €"
#: payment_methods/comnpay/views.py:53
#, python-format #, python-format
msgid "The payment of %(amount)s € has been accepted." msgid "The payment of %(amount)s € has been accepted."
msgstr "Le paiement de %(amount)s € a été accepté." msgstr "Le paiement de %(amount)s € a été accepté."
#: payment.py:49 #: payment_methods/comnpay/views.py:84
msgid "The payment has been refused." msgid "The payment has been refused."
msgstr "Le paiment a été refusé." msgstr "Le paiment a été refusé."
#: templates/cotisations/aff_article.html:31 #: templates/cotisations/aff_article.html:33
#: templates/cotisations/facture.html:43 #: templates/cotisations/facture.html:60
#: templates/cotisations/new_facture.html:50
#: templates/cotisations/new_facture_solde.html:44
msgid "Article" msgid "Article"
msgstr "Article" msgstr "Article"
#: templates/cotisations/aff_article.html:32 #: templates/cotisations/aff_article.html:34
msgid "Price" msgid "Price"
msgstr "Prix" msgstr "Prix"
#: templates/cotisations/aff_article.html:33 #: templates/cotisations/aff_article.html:35
msgid "Cotisation type" msgid "Cotisation type"
msgstr "Type de cotisation" msgstr "Type de cotisation"
#: templates/cotisations/aff_article.html:34 #: templates/cotisations/aff_article.html:36
msgid "Duration (month)" msgid "Duration (month)"
msgstr "Durée (mois)" msgstr "Durée (mois)"
#: templates/cotisations/aff_article.html:35 #: templates/cotisations/aff_article.html:37
msgid "Concerned users" msgid "Concerned users"
msgstr "Utilisateurs concernés" msgstr "Utilisateurs concernés"
#: templates/cotisations/aff_article.html:48 #: templates/cotisations/aff_article.html:38
#: templates/cotisations/aff_banque.html:40 msgid "Available for everyone"
#: templates/cotisations/aff_cotisations.html:69 msgstr "Articles disponibles"
#: templates/cotisations/aff_cotisations.html:75
#: templates/cotisations/aff_paiement.html:40 #: templates/cotisations/aff_article.html:52
#: templates/cotisations/control.html:104 views.py:396 views.py:443 #: templates/cotisations/aff_banque.html:41
#: views.py:507 views.py:585 #: templates/cotisations/aff_cotisations.html:70
#: templates/cotisations/aff_cotisations.html:76
#: templates/cotisations/aff_custom_invoice.html:73
#: templates/cotisations/aff_custom_invoice.html:79
#: templates/cotisations/aff_paiement.html:48
#: templates/cotisations/control.html:104 views.py:480 views.py:568
#: views.py:649
msgid "Edit" msgid "Edit"
msgstr "Modifier" msgstr "Modifier"
#: templates/cotisations/aff_article.html:52 #: templates/cotisations/aff_banque.html:32
#: templates/cotisations/aff_banque.html:44
#: templates/cotisations/aff_cotisations.html:90
#: templates/cotisations/aff_paiement.html:44
msgid "Historique"
msgstr "Historique"
#: templates/cotisations/aff_banque.html:31
msgid "Bank" msgid "Bank"
msgstr "Banque" msgstr "Banque"
#: templates/cotisations/aff_cotisations.html:37 #: templates/cotisations/aff_cotisations.html:38
msgid "User" msgid "User"
msgstr "Utilisateur" msgstr "Utilisateur"
#: templates/cotisations/aff_cotisations.html:40 #: templates/cotisations/aff_cotisations.html:41
#: templates/cotisations/aff_custom_invoice.html:42
#: templates/cotisations/control.html:60 #: templates/cotisations/control.html:60
#: templates/cotisations/edit_facture.html:45 #: templates/cotisations/edit_facture.html:45
msgid "Designation" msgid "Designation"
msgstr "Désignation" msgstr "Désignation"
#: templates/cotisations/aff_cotisations.html:41 #: templates/cotisations/aff_cotisations.html:42
#: templates/cotisations/aff_custom_invoice.html:43
#: templates/cotisations/control.html:61 #: templates/cotisations/control.html:61
msgid "Total price" msgid "Total price"
msgstr "Prix total" msgstr "Prix total"
#: templates/cotisations/aff_cotisations.html:43 #: templates/cotisations/aff_cotisations.html:44
#: templates/cotisations/aff_paiement.html:31 #: templates/cotisations/aff_custom_invoice.html:45
#: templates/cotisations/control.html:63 #: templates/cotisations/control.html:63
msgid "Payment method" msgid "Payment method"
msgstr "Moyen de paiement" msgstr "Moyen de paiement"
#: templates/cotisations/aff_cotisations.html:47 #: templates/cotisations/aff_cotisations.html:48
#: templates/cotisations/aff_custom_invoice.html:49
#: templates/cotisations/control.html:67 #: templates/cotisations/control.html:67
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
#: templates/cotisations/aff_cotisations.html:51 #: templates/cotisations/aff_cotisations.html:52
#: templates/cotisations/aff_custom_invoice.html:53
#: templates/cotisations/control.html:53 #: templates/cotisations/control.html:53
msgid "Invoice id" msgid "Invoice id"
msgstr "Id facture" msgstr "Id facture"
#: templates/cotisations/aff_cotisations.html:79 #: templates/cotisations/aff_cotisations.html:80
msgid "Controlled invoice" msgid "Controlled invoice"
msgstr "Facture controllé" msgstr "Facture controllé"
#: templates/cotisations/aff_cotisations.html:84 views.py:464 views.py:542 #: templates/cotisations/aff_cotisations.html:85
#: views.py:620 #: templates/cotisations/aff_custom_invoice.html:86 views.py:502 views.py:604
#: views.py:685
msgid "Delete" msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
#: templates/cotisations/aff_cotisations.html:99 #: templates/cotisations/aff_cotisations.html:98
#: templates/cotisations/aff_custom_invoice.html:98
msgid "PDF" msgid "PDF"
msgstr "PDF" msgstr "PDF"
#: templates/cotisations/aff_cotisations.html:102 #: templates/cotisations/aff_cotisations.html:101
msgid "Invalidated invoice" msgid "Invalidated invoice"
msgstr "Facture invalidée" msgstr "Facture invalidée"
#: templates/cotisations/aff_custom_invoice.html:39
msgid "Recipient"
msgstr "Destinataire"
#: templates/cotisations/aff_custom_invoice.html:56
msgid "Paid"
msgstr "Payé"
#: templates/cotisations/aff_paiement.html:33
msgid "Payment type"
msgstr "Type de paiement"
#: templates/cotisations/aff_paiement.html:34
msgid "Is available for everyone"
msgstr "Est disponible pour tout le monde"
#: templates/cotisations/aff_paiement.html:35
msgid "Custom payment method"
msgstr "Méthode de paiement personnalisée"
#: templates/cotisations/control.html:30 #: templates/cotisations/control.html:30
msgid "Invoice control" msgid "Invoice control"
msgstr "Contrôle des factures" msgstr "Contrôle des factures"
@ -394,15 +428,11 @@ msgstr ""
#: templates/cotisations/delete.html:40 #: templates/cotisations/delete.html:40
#: templates/cotisations/edit_facture.html:60 #: templates/cotisations/edit_facture.html:60
#: templates/cotisations/new_facture_solde.html:59
#: templates/cotisations/recharge.html:42
msgid "Confirm" msgid "Confirm"
msgstr "Confirmer" msgstr "Confirmer"
#: templates/cotisations/edit_facture.html:31 #: templates/cotisations/edit_facture.html:31
#: templates/cotisations/facture.html:30 #: templates/cotisations/facture.html:30
#: templates/cotisations/new_facture.html:30
#: templates/cotisations/new_facture_solde.html:30
msgid "Invoices creation and edition" msgid "Invoices creation and edition"
msgstr "Création et modification de factures" msgstr "Création et modification de factures"
@ -411,9 +441,7 @@ msgid "Edit the invoice"
msgstr "Edition de factures" msgstr "Edition de factures"
#: templates/cotisations/edit_facture.html:41 #: templates/cotisations/edit_facture.html:41
#: templates/cotisations/facture.html:38 #: templates/cotisations/facture.html:55
#: templates/cotisations/new_facture.html:46
#: templates/cotisations/new_facture_solde.html:40
msgid "Invoice's articles" msgid "Invoice's articles"
msgstr "Articles de la facture" msgstr "Articles de la facture"
@ -421,15 +449,23 @@ msgstr "Articles de la facture"
msgid "Quantity" msgid "Quantity"
msgstr "Quantité" msgstr "Quantité"
#: templates/cotisations/facture.html:52 #: templates/cotisations/facture.html:36
#: templates/cotisations/new_facture.html:59 msgid "New invoice"
#: templates/cotisations/new_facture_solde.html:53 msgstr "Nouvelle facture"
#: templates/cotisations/facture.html:39
msgid "Maximum allowed balance : "
msgstr "Solde maximum autorisé : "
#: templates/cotisations/facture.html:43
msgid "Current balance :"
msgstr "Solde actuel :"
#: templates/cotisations/facture.html:69
msgid "Add an article" msgid "Add an article"
msgstr "Ajouter un article" msgstr "Ajouter un article"
#: templates/cotisations/facture.html:54 #: templates/cotisations/facture.html:71
#: templates/cotisations/new_facture.html:61
#: templates/cotisations/new_facture_solde.html:55
msgid "" msgid ""
"\n" "\n"
" Total price : <span id=\"total_price\">0,00</span> €\n" " Total price : <span id=\"total_price\">0,00</span> €\n"
@ -464,7 +500,7 @@ msgid "Delete article types"
msgstr "Supprimer des types d'articles" msgstr "Supprimer des types d'articles"
#: templates/cotisations/index_banque.html:30 #: templates/cotisations/index_banque.html:30
#: templates/cotisations/sidebar.html:50 #: templates/cotisations/sidebar.html:55
msgid "Banks" msgid "Banks"
msgstr "Banques" msgstr "Banques"
@ -480,6 +516,15 @@ msgstr "Ajouter une banque"
msgid "Delete banks" msgid "Delete banks"
msgstr "Supprimer des banques" msgstr "Supprimer des banques"
#: templates/cotisations/index_custom_invoice.html:28
#: templates/cotisations/sidebar.html:45
msgid "Custom invoices"
msgstr "Factures personnalisées"
#: templates/cotisations/index_custom_invoice.html:31
msgid "Custom invoices list"
msgstr "Liste des factures personalisées"
#: templates/cotisations/index_paiement.html:30 #: templates/cotisations/index_paiement.html:30
msgid "Payments" msgid "Payments"
msgstr "Paiement" msgstr "Paiement"
@ -496,59 +541,24 @@ msgstr "Ajouter un type de paiement"
msgid "Delete payment types" msgid "Delete payment types"
msgstr "Supprimer un type de paiement" msgstr "Supprimer un type de paiement"
#: templates/cotisations/new_facture.html:37 #: templates/cotisations/payment.html:30
#: templates/cotisations/new_facture_solde.html:37
msgid "New invoice"
msgstr "Nouvelle facture"
#: templates/cotisations/new_facture.html:39
#, python-format
msgid ""
"\n"
" User's balance : %(user.solde)s €\n"
" "
msgstr ""
"\n"
" Solde de l'utilisateur : %(user.solde)s €\n"
" "
#: templates/cotisations/new_facture.html:65 views.py:257
msgid "Create"
msgstr "Créer"
#: templates/cotisations/payment.html:30 templates/cotisations/recharge.html:30
#: templates/cotisations/recharge.html:33
msgid "Balance refill" msgid "Balance refill"
msgstr "Rechargement de solde" msgstr "Rechargement de solde"
#: templates/cotisations/payment.html:34 #: templates/cotisations/payment.html:34
#, python-format
msgid "" msgid ""
"\n" "\n"
" Refill of %(amount)s €\n" " Pay %(amount)s €\n"
" " " "
msgstr "" msgstr ""
"\n" "\n"
" Recharger de %(amount)s €\n" " Recharger de %(amount)s €\n"
" " " "
#: templates/cotisations/payment.html:40 #: templates/cotisations/payment.html:44 views.py:867
msgid "Pay" msgid "Pay"
msgstr "Payer" msgstr "Payer"
#: templates/cotisations/recharge.html:35
#, python-format
msgid ""
"\n"
" Balance : <span class=\"label label-default\">%(request.user.solde)s "
"€</span>\n"
" "
msgstr ""
"\n"
" Solde : <span class=\"label label-default\">%(request.user.solde)s "
"€</span>\n"
" "
#: templates/cotisations/sidebar.html:32 #: templates/cotisations/sidebar.html:32
msgid "Create an invoice" msgid "Create an invoice"
msgstr "Créer une facture" msgstr "Créer une facture"
@ -557,83 +567,94 @@ msgstr "Créer une facture"
msgid "Control the invoices" msgid "Control the invoices"
msgstr "Contrôler les factures" msgstr "Contrôler les factures"
#: templates/cotisations/sidebar.html:45 #: templates/cotisations/sidebar.html:50
msgid "Available articles" msgid "Available articles"
msgstr "Articles disponibles" msgstr "Articles disponibles"
#: templates/cotisations/sidebar.html:55 #: templates/cotisations/sidebar.html:60
msgid "Payment methods" msgid "Payment methods"
msgstr "Moyens de paiement" msgstr "Moyens de paiement"
#: views.py:138 #: validators.py:20
msgid "Your balance is too low for this operation." msgid "There are already payment method(s) for user balance"
msgstr "Votre solde est trop faible pour cette opération." msgstr "Il y a déjà une méthode de paiement pour le solde utilisateur"
#: views.py:168 #: views.py:165
#, python-format
msgid ""
"The cotisation of %(member_name)s has been extended to "
"%(end_date)s."
msgstr "La cotisation de %(member_name)s a été étendu jusqu'à %(end_date)s."
#: views.py:178
msgid "The invoice has been created."
msgstr "La facture a été créée."
#: views.py:186 views.py:824
msgid "You need to choose at least one article." msgid "You need to choose at least one article."
msgstr "Vous devez choisir au moins un article." msgstr "Vous devez choisir au moins un article."
#: views.py:338 #: views.py:178 views.py:232
msgid "Create"
msgstr "Créer"
#: views.py:225
msgid "The custom invoice was successfully created."
msgstr "La facture a été créée avec succès."
#: views.py:313 views.py:367
msgid "The invoice has been successfully edited." msgid "The invoice has been successfully edited."
msgstr "La facture a été crée avec succès." msgstr "La facture a été crée avec succès."
#: views.py:358 #: views.py:333 views.py:427
msgid "The invoice has been successfully deleted." msgid "The invoice has been successfully deleted."
msgstr "La facture a été supprimée avec succès." msgstr "La facture a été supprimée avec succès."
#: views.py:363 #: views.py:338 views.py:432
msgid "Invoice" msgid "Invoice"
msgstr "Facture" msgstr "Facture"
#: views.py:391 #: views.py:453
msgid "Balance successfully updated."
msgstr "Solde mis à jour avec succès."
#: views.py:417
msgid "The article has been successfully created." msgid "The article has been successfully created."
msgstr "L'article a été créé avec succès." msgstr "L'article a été créé avec succès."
#: views.py:422 views.py:485 views.py:563 #: views.py:458 views.py:531 views.py:626
#, fuzzy msgid "Address"
#| msgid "Address"
msgid "Add"
msgstr "Adresse" msgstr "Adresse"
#: views.py:438 #: views.py:459
msgid "New article"
msgstr "Nouvel article"
#: views.py:475
msgid "The article has been successfully edited." msgid "The article has been successfully edited."
msgstr "L'article a été modifié avec succès." msgstr "L'article a été modifié avec succès."
#: views.py:459 #: views.py:481
msgid "Edit article"
msgstr "Éditer l'article"
#: views.py:497
msgid "The article(s) have been successfully deleted." msgid "The article(s) have been successfully deleted."
msgstr "L'(es) article(s) a(ont) été supprimé(s) avec succès. " msgstr "L'(es) article(s) a(ont) été supprimé(s) avec succès. "
#: views.py:480 #: views.py:503
msgid "Delete article"
msgstr "Supprimer l'article"
#: views.py:525
msgid "The payment method has been successfully created." msgid "The payment method has been successfully created."
msgstr "Le moyen de paiement a été créé avec succès." msgstr "Le moyen de paiement a été créé avec succès."
#: views.py:502 #: views.py:532
msgid "New payment method"
msgstr "Nouveau moyen de paiement"
#: views.py:562
msgid "The payement method has been successfully edited." msgid "The payement method has been successfully edited."
msgstr "Le moyen de paiement a été modifié avec succès." msgstr "Le moyen de paiement a été modifié avec succès."
#: views.py:526 #: views.py:569
msgid "Edit payment method"
msgstr "Éditer le moyen de paiement"
#: views.py:588
#, python-format #, python-format
msgid "" msgid ""
"The payment method %(method_name)s has been successfully " "The payment method %(method_name)s has been successfully "
"deleted." "deleted."
msgstr "Le moyen de paiement %(method_name)s a été supprimé avec succès." msgstr "Le moyen de paiement %(method_name)s a été supprimé avec succès."
#: views.py:534 #: views.py:596
#, python-format #, python-format
msgid "" msgid ""
"The payment method %(method_name)s can't be deleted " "The payment method %(method_name)s can't be deleted "
@ -642,21 +663,33 @@ msgstr ""
"Le moyen de paiement %(method_name)s ne peut pas être mis à jour car il y a " "Le moyen de paiement %(method_name)s ne peut pas être mis à jour car il y a "
"des factures l'utilisant." "des factures l'utilisant."
#: views.py:558 #: views.py:605
msgid "Delete payment method"
msgstr "Supprimer le moyen de paiement"
#: views.py:621
msgid "The bank has been successfully created." msgid "The bank has been successfully created."
msgstr "La banque a été crée avec succès." msgstr "La banque a été crée avec succès."
#: views.py:580 #: views.py:627
msgid "New bank"
msgstr "Créer la banque"
#: views.py:644
msgid "The bank has been successfully edited" msgid "The bank has been successfully edited"
msgstr "La banque a été modifée avec succès." msgstr "La banque a été modifée avec succès."
#: views.py:604 #: views.py:650
msgid "Edit bank"
msgstr "Éditer la banque"
#: views.py:669
#, python-format #, python-format
msgid "" msgid ""
"The bank %(bank_name)s has been successfully deleted." "The bank %(bank_name)s has been successfully deleted."
msgstr "La banque %(bank_name)s a été supprimée avec succès." msgstr "La banque %(bank_name)s a été supprimée avec succès."
#: views.py:612 #: views.py:677
#, python-format #, python-format
msgid "" msgid ""
"The bank %(bank_name)s can't be deleted because there " "The bank %(bank_name)s can't be deleted because there "
@ -665,127 +698,131 @@ msgstr ""
"La banque %(bank_name)s ne peut pas être supprimée car il y a des factures " "La banque %(bank_name)s ne peut pas être supprimée car il y a des factures "
"qui l'utilisent." "qui l'utilisent."
#: views.py:656 #: views.py:686
msgid "Delete bank"
msgstr "Supprimer la banque"
#: views.py:722
msgid "Your changes have been properly taken into account." msgid "Your changes have been properly taken into account."
msgstr "Vos modifications ont correctement été prises en compte." msgstr "Vos modifications ont correctement été prises en compte."
#: views.py:776 #: views.py:834
msgid "The balance is too low for this operation." msgid "You are not allowed to credit your balance."
msgstr "Le solde est trop faible pour cette opération." msgstr "Vous n'êtes pas autorisé à créditer votre solde."
#: views.py:806 #: views.py:866
#, python-format msgid "Refill your balance"
msgid "" msgstr "Créditer votre solde"
"The cotisation of %(member_name)s has been successfully "
"extended to %(end_date)s."
msgstr "La cotisation de %(member_name)s a été prolongée jusqu'à %(end_date)s."
#: views.py:816 #: models.py:137
msgid "The invoice has been successuflly created." msgid "Cheque number"
msgstr "La facture a été créée avec succès." msgstr "Numéro de chèque"
#: views.py:846 msgid "Not specified"
msgid "Online payment is disabled." msgstr "Non renseigné"
msgstr "Le paiement en ligne est désactivé."
#~ msgid "Paid" msgid "A cheque number and a bank must be specified."
#~ msgstr "Payé" msgstr "Un numéro de chèqe et une banque doivent être renseignés."
#~ msgid "Recipient" #: models.py:155
#~ msgstr "Destinataire" msgid "Can change the \"controlled\" state"
msgstr "Peut modifier l'état \"controllé\""
#~ msgid "Invoice number" #: models.py:157
#~ msgstr "Numéro de facture" msgid "Can see an invoice's details"
msgstr "Peut voir les détails d'une facture"
#~ msgid "Existing articles" #: models.py:159
#~ msgstr "Articles disponibles" msgid "Can edit all the previous invoices"
msgstr "Peut modifier toutes les factures existantes"
#~ msgid "Existing payment method" #: models.py:297
#~ msgstr "Moyen de paiements disponibles" msgid "Connexion"
msgstr "Connexion"
#~ msgid "Existing banks" #: models.py:336
#~ msgstr "Banques disponibles" msgid "Membership"
msgstr "Adhésion"
#~ msgid "Amount" #: models.py:299
#~ msgstr "Montant" msgid "Both of them"
msgstr "Les deux"
#~ msgid "Can change the \"controlled\" state" #: models.py:328
#~ msgstr "Peut modifier l'état \"controllé\"" msgid "Duration (in whole month)"
msgstr "Durée (en mois entiers)"
#~ msgid "Can create a custom PDF invoice" #: models.py:336
#~ msgstr "Peut crée une facture PDF personnalisée" msgid "Type of cotisation"
msgstr "Type de cotisation"
#~ msgid "Can see an invoice's details" #: models.py:341
#~ msgstr "Peut voir les détails d'une facture" msgid "Can see a purchase's details"
msgstr "Peut voir les détails d'un achat"
#~ msgid "Can edit all the previous invoices" #: models.py:342
#~ msgstr "Peut modifier toutes les factures existantes" msgid "Can edit all the previous purchases"
msgstr "Peut voir les achats existants"
#~ msgid "Connexion" #: models.py:344
#~ msgstr "Connexion" msgid "Purchase"
msgstr "Achat"
#~ msgid "Membership" #: models.py:345
#~ msgstr "Adhésion" msgid "Purchases"
msgstr "Achat"
#~ msgid "Both of them" #: models.py:512
#~ msgstr "Les deux" msgid "Club"
msgstr "Club"
#~ msgid "Duration (in whole month)" #: models.py:530
#~ msgstr "Durée (en mois entiers)" msgid "Unitary price"
msgstr "Prix unitaire"
#~ msgid "Type of cotisation" #: models.py:538
#~ msgstr "Type de cotisation" msgid "Type of users concerned"
msgstr "Type d'utilisateurs concernés"
#~ msgid "Can see a purchase's details" #: models.py:561
#~ msgstr "Peut voir les détails d'un achat" msgid "Can see an article's details"
msgstr "Peut voir les détails d'un article"
#~ msgid "Can edit all the previous purchases" #: models.py:621
#~ msgstr "Peut voir les achats existants" msgid "Name"
msgstr "Nom"
#~ msgid "Purchase" #: models.py:626
#~ msgstr "Achat" msgid "Can see a bank's details"
msgstr "Peut voir les détails d'une banque"
#~ msgid "Purchases" #: models.py:344
#~ msgstr "Achat" msgid "Standard"
msgstr "Standard"
#~ msgid "Club" msgid "Cheque"
#~ msgstr "Club" msgstr "Chèque"
#~ msgid "Unitary price" #: models.py:647
#~ msgstr "Prix unitaire" msgid "Method"
msgstr "Moyen"
#~ msgid "Type of users concerned" #: models.py:663
#~ msgstr "Type d'utilisateurs concernés" msgid "Can see a payement's details"
msgstr "Peut voir les détails d'un paiement"
#~ msgid "Can see an article's details" #: models.py:785
#~ msgstr "Peut voir les détails d'un article" msgid "Starting date"
msgstr "Date de début"
#~ msgid "Name" #: models.py:788
#~ msgstr "Nom" msgid "Ending date"
msgstr "Date de fin"
#~ msgid "Can see a bank's details" #: models.py:793
#~ msgstr "Peut voir les détails d'une banque" msgid "Can see a cotisation's details"
msgstr "Peut voir les détails d'une cotisation"
#~ msgid "Standard" #: models.py:794
#~ msgstr "Standard" msgid "Can edit the previous cotisations"
msgstr "Peut voir les cotisations existantes"
#~ msgid "Cheque"
#~ msgstr "Chèque"
#~ msgid "Method"
#~ msgstr "Moyen"
#~ msgid "Can see a payement's details"
#~ msgstr "Peut voir les détails d'un paiement"
#~ msgid "Starting date"
#~ msgstr "Date de début"
#~ msgid "Ending date"
#~ msgstr "Date de fin"
#~ msgid "Can see a cotisation's details"
#~ msgstr "Peut voir les détails d'une cotisation"
#~ msgid "Can edit the previous cotisations"
#~ msgstr "Peut voir les cotisations existantes"

View file

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-21 20:01
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.contrib.auth.management import create_permissions
import re2o.field_permissions
import re2o.mixins
def reattribute_ids(apps, schema_editor):
Facture = apps.get_model('cotisations', 'Facture')
BaseInvoice = apps.get_model('cotisations', 'BaseInvoice')
for f in Facture.objects.all():
base = BaseInvoice.objects.create(id=f.pk, date=f.date)
f.baseinvoice_ptr = base
f.save()
def update_rights(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
# creates needed permissions
app = apps.get_app_config('cotisations')
app.models_module = True
create_permissions(app)
app.models_module = False
former = Permission.objects.get(codename='change_facture_pdf')
new_1 = Permission.objects.get(codename='add_custominvoice')
new_2 = Permission.objects.get(codename='change_custominvoice')
new_3 = Permission.objects.get(codename='view_custominvoice')
new_4 = Permission.objects.get(codename='delete_custominvoice')
for group in former.group_set.all():
group.permissions.remove(former)
group.permissions.add(new_1)
group.permissions.add(new_2)
group.permissions.add(new_3)
group.permissions.add(new_4)
group.save()
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0031_comnpaypayment_production'),
]
operations = [
migrations.CreateModel(
name='BaseInvoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
],
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, re2o.field_permissions.FieldPermissionModelMixin, models.Model),
),
migrations.CreateModel(
name='CustomInvoice',
fields=[
('baseinvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice')),
('recipient', models.CharField(max_length=255, verbose_name='Recipient')),
('payment', models.CharField(max_length=255, verbose_name='Payment type')),
('address', models.CharField(max_length=255, verbose_name='Address')),
('paid', models.BooleanField(verbose_name='Paid')),
],
bases=('cotisations.baseinvoice',),
options={'permissions': (('view_custominvoice', 'Can view a custom invoice'),)},
),
migrations.AddField(
model_name='facture',
name='baseinvoice_ptr',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='cotisations.BaseInvoice', null=True),
preserve_default=False,
),
migrations.RunPython(reattribute_ids),
migrations.AlterField(
model_name='vente',
name='facture',
field=models.ForeignKey(on_delete=models.CASCADE, verbose_name='Invoice', to='cotisations.BaseInvoice')
),
migrations.RemoveField(
model_name='facture',
name='id',
),
migrations.RemoveField(
model_name='facture',
name='date',
),
migrations.AlterField(
model_name='facture',
name='baseinvoice_ptr',
field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice'),
),
migrations.RunPython(update_rights),
migrations.AlterModelOptions(
name='facture',
options={'permissions': (('change_facture_control', 'Can change the "controlled" state'), ('view_facture', "Can see an invoice's details"), ('change_all_facture', 'Can edit all the previous invoices')), 'verbose_name': 'Invoice', 'verbose_name_plural': 'Invoices'},
),
]

View file

@ -55,80 +55,11 @@ from cotisations.utils import find_payment_method
from cotisations.validators import check_no_balance from cotisations.validators import check_no_balance
# TODO : change facture to invoice class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
"""
The model for an invoice. It reprensents the fact that a user paid for
something (it can be multiple article paid at once).
An invoice is linked to :
* one or more purchases (one for each article sold that time)
* a user (the one who bought those articles)
* a payment method (the one used by the user)
* (if applicable) a bank
* (if applicable) a cheque number.
Every invoice is dated throught the 'date' value.
An invoice has a 'controlled' value (default : False) which means that
someone with high enough rights has controlled that invoice and taken it
into account. It also has a 'valid' value (default : True) which means
that someone with high enough rights has decided that this invoice was not
valid (thus it's like the user never paid for his articles). It may be
necessary in case of non-payment.
"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_l("Cheque number")
)
date = models.DateTimeField( date = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name=_l("Date") verbose_name=_l("Date")
) )
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_l("Validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_l("Controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control',
_l("Can change the \"controlled\" state")),
# TODO : seems more likely to be call create_facture_pdf
# or create_invoice_pdf
('change_facture_pdf',
_l("Can create a custom PDF invoice")),
('view_facture',
_l("Can see an invoice's details")),
('change_all_facture',
_l("Can edit all the previous invoices")),
)
verbose_name = _l("Invoice")
verbose_name_plural = _l("Invoices")
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
# TODO : change prix to price # TODO : change prix to price
def prix(self): def prix(self):
@ -167,6 +98,74 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
).values_list('name', flat=True)) ).values_list('name', flat=True))
return name return name
# TODO : change facture to invoice
class Facture(BaseInvoice):
"""
The model for an invoice. It reprensents the fact that a user paid for
something (it can be multiple article paid at once).
An invoice is linked to :
* one or more purchases (one for each article sold that time)
* a user (the one who bought those articles)
* a payment method (the one used by the user)
* (if applicable) a bank
* (if applicable) a cheque number.
Every invoice is dated throught the 'date' value.
An invoice has a 'controlled' value (default : False) which means that
someone with high enough rights has controlled that invoice and taken it
into account. It also has a 'valid' value (default : True) which means
that someone with high enough rights has decided that this invoice was not
valid (thus it's like the user never paid for his articles). It may be
necessary in case of non-payment.
"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_l("Cheque number")
)
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_l("Validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_l("Controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control',
_l("Can change the \"controlled\" state")),
('view_facture',
_l("Can see an invoice's details")),
('change_all_facture',
_l("Can edit all the previous invoices")),
)
verbose_name = _l("Invoice")
verbose_name_plural = _l("Invoices")
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
def can_edit(self, user_request, *args, **kwargs): def can_edit(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.change_facture'): if not user_request.has_perm('cotisations.change_facture'):
return False, _("You don't have the right to edit an invoice.") return False, _("You don't have the right to edit an invoice.")
@ -212,14 +211,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
_("You don't have the right to edit the \"controlled\" state.") _("You don't have the right to edit the \"controlled\" state.")
) )
@staticmethod
def can_change_pdf(user_request, *_args, **_kwargs):
""" Returns True if the user can change this invoice """
return (
user_request.has_perm('cotisations.change_facture_pdf'),
_("You don't have the right to edit an invoice.")
)
@staticmethod @staticmethod
def can_create(user_request, *_args, **_kwargs): def can_create(user_request, *_args, **_kwargs):
"""Check if a user can create an invoice. """Check if a user can create an invoice.
@ -265,6 +256,28 @@ def facture_post_delete(**kwargs):
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class CustomInvoice(BaseInvoice):
class Meta:
permissions = (
('view_custominvoice', _l("Can view a custom invoice")),
)
recipient = models.CharField(
max_length=255,
verbose_name=_l("Recipient")
)
payment = models.CharField(
max_length=255,
verbose_name=_l("Payment type")
)
address = models.CharField(
max_length=255,
verbose_name=_l("Address")
)
paid = models.BooleanField(
verbose_name="Paid"
)
# TODO : change Vente to Purchase # TODO : change Vente to Purchase
class Vente(RevMixin, AclMixin, models.Model): class Vente(RevMixin, AclMixin, models.Model):
""" """
@ -288,7 +301,7 @@ class Vente(RevMixin, AclMixin, models.Model):
# TODO : change facture to invoice # TODO : change facture to invoice
facture = models.ForeignKey( facture = models.ForeignKey(
'Facture', 'BaseInvoice',
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_l("Invoice") verbose_name=_l("Invoice")
) )
@ -355,6 +368,10 @@ class Vente(RevMixin, AclMixin, models.Model):
cotisation_type defined (which means the article sold represents cotisation_type defined (which means the article sold represents
a cotisation) a cotisation)
""" """
try:
invoice = self.facture.facture
except Facture.DoesNotExist:
return
if not hasattr(self, 'cotisation') and self.type_cotisation: if not hasattr(self, 'cotisation') and self.type_cotisation:
cotisation = Cotisation(vente=self) cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation cotisation.type_cotisation = self.type_cotisation
@ -362,7 +379,7 @@ class Vente(RevMixin, AclMixin, models.Model):
end_cotisation = Cotisation.objects.filter( end_cotisation = Cotisation.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter( facture__in=Facture.objects.filter(
user=self.facture.user user=invoice.user
).exclude(valid=False)) ).exclude(valid=False))
).filter( ).filter(
Q(type_cotisation='All') | Q(type_cotisation='All') |
@ -371,9 +388,9 @@ class Vente(RevMixin, AclMixin, models.Model):
date_start__lt=date_start date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max'] ).aggregate(Max('date_end'))['date_end__max']
elif self.type_cotisation == "Adhesion": elif self.type_cotisation == "Adhesion":
end_cotisation = self.facture.user.end_adhesion() end_cotisation = invoice.user.end_adhesion()
else: else:
end_cotisation = self.facture.user.end_connexion() end_cotisation = invoice.user.end_connexion()
date_start = date_start or timezone.now() date_start = date_start or timezone.now()
end_cotisation = end_cotisation or date_start end_cotisation = end_cotisation or date_start
date_max = max(end_cotisation, date_start) date_max = max(end_cotisation, date_start)
@ -445,6 +462,10 @@ def vente_post_save(**kwargs):
LDAP user when a purchase has been saved. LDAP user when a purchase has been saved.
""" """
purchase = kwargs['instance'] purchase = kwargs['instance']
try:
purchase.facture.facture
except Facture.DoesNotExist:
return
if hasattr(purchase, 'cotisation'): if hasattr(purchase, 'cotisation'):
purchase.cotisation.vente = purchase purchase.cotisation.vente = purchase
purchase.cotisation.save() purchase.cotisation.save()
@ -462,8 +483,12 @@ def vente_post_delete(**kwargs):
Synchronise the LDAP user after a purchase has been deleted. Synchronise the LDAP user after a purchase has been deleted.
""" """
purchase = kwargs['instance'] purchase = kwargs['instance']
try:
invoice = purchase.facture.facture
except Facture.DoesNotExist:
return
if purchase.type_cotisation: if purchase.type_cotisation:
user = purchase.facture.user user = invoice.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)

View file

@ -0,0 +1,89 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2018 Hugo Levy-Falk
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load i18n %}
{% load acl %}
{% load logs_extra %}
{% load design %}
<div class="table-responsive">
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>
{% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
</th>
<th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
<th>
{% trans "Payment method" as tr_payment_method %}
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
</th>
<th>
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
</th>
<th>
{% trans "Invoice id" as tr_invoice_id %}
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %}
</th>
<th>
{% trans "Paid" as tr_invoice_paid%}
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_paid %}
</th>
<th></th>
<th></th>
</tr>
</thead>
{% for invoice in custom_invoice_list %}
<tr>
<td>{{ invoice.recipient }}</td>
<td>{{ invoice.name }}</td>
<td>{{ invoice.prix_total }}</td>
<td>{{ invoice.payment }}</td>
<td>{{ invoice.date }}</td>
<td>{{ invoice.id }}</td>
<td>{{ invoice.paid|tick }}</td>
<td>
{% can_edit invoice %}
{% include 'buttons/edit.html' with href='cotisations:edit-custom-invoice' id=invoice.id %}
{% acl_end %}
{% can_delete invoice %}
{% include 'buttons/suppr.html' with href='cotisations:del-custom-invoice' id=invoice.id %}
{% acl_end %}
{% history_button invoice %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:custom-invoice-pdf' invoice.id %}">
<i class="fa fa-file-pdf"></i> {% trans "PDF" %}
</a>
</td>
</tr>
{% endfor %}
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -0,0 +1,36 @@
{% extends "cotisations/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load acl %}
{% load i18n %}
{% block title %}{% trans "Custom invoices" %}{% endblock %}
{% block content %}
<h2>{% trans "Custom invoices list" %}</h2>
{% can_create CustomInvoice %}
{% include "buttons/add.html" with href='cotisations:new-custom-invoice'%}
{% acl_end %}
{% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %}
{% endblock %}

View file

@ -27,8 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load i18n %} {% load i18n %}
{% block sidebar %} {% block sidebar %}
{% can_change Facture pdf %} {% can_create CustomInvoice %}
<a class="list-group-item list-group-item-success" href="{% url "cotisations:new-facture-pdf" %}"> <a class="list-group-item list-group-item-success" href="{% url "cotisations:new-custom-invoice" %}">
<i class="fa fa-plus"></i> {% trans "Create an invoice" %} <i class="fa fa-plus"></i> {% trans "Create an invoice" %}
</a> </a>
<a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}"> <a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}">
@ -40,6 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-list-ul"></i> {% trans "Invoices" %} <i class="fa fa-list-ul"></i> {% trans "Invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all CustomInvoice %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-custom-invoice" %}">
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a>
{% acl_end %}
{% can_view_all Article %} {% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %} <i class="fa fa-list-ul"></i> {% trans "Available articles" %}

View file

@ -105,7 +105,7 @@ def render_tex(_request, template, ctx={}):
Returns: Returns:
An HttpResponse with type `application/pdf` containing the PDF file. An HttpResponse with type `application/pdf` containing the PDF file.
""" """
pdf = create_pdf(template, ctx={}) pdf = create_pdf(template, ctx)
r = HttpResponse(content_type='application/pdf') r = HttpResponse(content_type='application/pdf')
r.write(pdf) r.write(pdf)
return r return r

View file

@ -52,9 +52,29 @@ urlpatterns = [
name='facture-pdf' name='facture-pdf'
), ),
url( url(
r'^new_facture_pdf/$', r'^index_custom_invoice/$',
views.new_facture_pdf, views.index_custom_invoice,
name='new-facture-pdf' name='index-custom-invoice'
),
url(
r'^new_custom_invoice/$',
views.new_custom_invoice,
name='new-custom-invoice'
),
url(
r'^edit_custom_invoice/(?P<custominvoiceid>[0-9]+)$',
views.edit_custom_invoice,
name='edit-custom-invoice'
),
url(
r'^custom_invoice_pdf/(?P<custominvoiceid>[0-9]+)$',
views.custom_invoice_pdf,
name='custom-invoice-pdf',
),
url(
r'^del_custom_invoice/(?P<custominvoiceid>[0-9]+)$',
views.del_custom_invoice,
name='del-custom-invoice'
), ),
url( url(
r'^credit_solde/(?P<userid>[0-9]+)$', r'^credit_solde/(?P<userid>[0-9]+)$',

View file

@ -58,7 +58,15 @@ from re2o.acl import (
can_change, can_change,
) )
from preferences.models import AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque from .models import (
Facture,
Article,
Vente,
Paiement,
Banque,
CustomInvoice,
BaseInvoice
)
from .forms import ( from .forms import (
FactureForm, FactureForm,
ArticleForm, ArticleForm,
@ -67,10 +75,10 @@ from .forms import (
DelPaiementForm, DelPaiementForm,
BanqueForm, BanqueForm,
DelBanqueForm, DelBanqueForm,
NewFactureFormPdf,
SelectUserArticleForm, SelectUserArticleForm,
SelectClubArticleForm, SelectClubArticleForm,
RechargeForm RechargeForm,
CustomInvoiceForm
) )
from .tex import render_invoice from .tex import render_invoice
from .payment_methods.forms import payment_method_factory from .payment_methods.forms import payment_method_factory
@ -178,10 +186,10 @@ def new_facture(request, user, userid):
# TODO : change facture to invoice # TODO : change facture to invoice
@login_required @login_required
@can_change(Facture, 'pdf') @can_create(CustomInvoice)
def new_facture_pdf(request): def new_custom_invoice(request):
""" """
View used to generate a custom PDF invoice. It's mainly used to View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative get invoices that are not taken into account, for the administrative
point of view. point of view.
""" """
@ -190,7 +198,7 @@ def new_facture_pdf(request):
Q(type_user='All') | Q(type_user=request.user.class_name) Q(type_user='All') | Q(type_user=request.user.class_name)
) )
# Building the invocie form and the article formset # Building the invocie form and the article formset
invoice_form = NewFactureFormPdf(request.POST or None) invoice_form = CustomInvoiceForm(request.POST or None)
if request.user.is_class_club: if request.user.is_class_club:
articles_formset = formset_factory(SelectClubArticleForm)( articles_formset = formset_factory(SelectClubArticleForm)(
request.POST or None, request.POST or None,
@ -202,44 +210,31 @@ def new_facture_pdf(request):
form_kwargs={'user': request.user} form_kwargs={'user': request.user}
) )
if invoice_form.is_valid() and articles_formset.is_valid(): if invoice_form.is_valid() and articles_formset.is_valid():
# Get the article list and build an list out of it new_invoice_instance = invoice_form.save()
# contiaining (article_name, article_price, quantity, total_price) for art_item in articles_formset:
articles_info = [] if art_item.cleaned_data:
for articles_form in articles_formset: article = art_item.cleaned_data['article']
if articles_form.cleaned_data: quantity = art_item.cleaned_data['quantity']
article = articles_form.cleaned_data['article'] Vente.objects.create(
quantity = articles_form.cleaned_data['quantity'] facture=new_invoice_instance,
articles_info.append({ name=article.name,
'name': article.name, prix=article.prix,
'price': article.prix, type_cotisation=article.type_cotisation,
'quantity': quantity, duration=article.duration,
'total_price': article.prix * quantity number=quantity
}) )
paid = invoice_form.cleaned_data['paid'] messages.success(
recipient = invoice_form.cleaned_data['dest'] request,
address = invoice_form.cleaned_data['chambre'] _('The custom invoice was successfully created.')
total_price = sum(a['total_price'] for a in articles_info) )
return redirect(reverse('cotisations:index-custom-invoice'))
return render_invoice(request, {
'DATE': timezone.now(),
'recipient_name': recipient,
'address': address,
'article': articles_info,
'total': total_price,
'paid': paid,
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
return form({ return form({
'factureform': invoice_form, 'factureform': invoice_form,
'action_name': _("Create"), 'action_name': _("Create"),
'articlesformset': articles_formset, 'articlesformset': articles_formset,
'articles': articles 'articlelist': articles
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -292,7 +287,7 @@ def facture_pdf(request, facture, **_kwargs):
def edit_facture(request, facture, **_kwargs): def edit_facture(request, facture, **_kwargs):
""" """
View used to edit an existing invoice. View used to edit an existing invoice.
Articles can be added or remove to the invoice and quantity Articles can be added or removed to the invoice and quantity
can be set as desired. This is also the view used to invalidate can be set as desired. This is also the view used to invalidate
an invoice. an invoice.
""" """
@ -347,6 +342,100 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request) }, 'cotisations/delete.html', request)
@login_required
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CustomInvoiceForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(purchases_objects)
)
purchase_form = purchase_form_set(
request.POST or None,
queryset=purchases_objects
)
if invoice_form.is_valid() and purchase_form.is_valid():
if invoice_form.changed_data:
invoice_form.save()
purchase_form.save()
messages.success(
request,
_("The invoice has been successfully edited.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'factureform': invoice_form,
'venteform': purchase_form
}, 'cotisations/edit_facture.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing invoice in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': purchase.name,
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
# TODO : change facture to invoice
@login_required
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
invoice.delete()
messages.success(
request,
_("The invoice has been successfully deleted.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'objet': invoice,
'objet_name': _("Invoice")
}, 'cotisations/delete.html', request)
@login_required @login_required
@can_create(Article) @can_create(Article)
def add_article(request): def add_article(request):
@ -681,8 +770,31 @@ def index_banque(request):
}) })
@login_required
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
custom_invoice_list = SortTable.sort(
custom_invoice_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
custom_invoice_list = re2o_paginator(
request,
custom_invoice_list,
pagination_number,
)
return render(request, 'cotisations/index_custom_invoice.html', {
'custom_invoice_list': custom_invoice_list
})
@login_required @login_required
@can_view_all(Facture) @can_view_all(Facture)
@can_view_all(CustomInvoice)
def index(request): def index(request):
""" """
View used to display the list of all exisitng invoices. View used to display the list of all exisitng invoices.
@ -698,7 +810,7 @@ def index(request):
) )
invoice_list = re2o_paginator(request, invoice_list, pagination_number) invoice_list = re2o_paginator(request, invoice_list, pagination_number)
return render(request, 'cotisations/index.html', { return render(request, 'cotisations/index.html', {
'facture_list': invoice_list 'facture_list': invoice_list,
}) })

View file

@ -250,6 +250,14 @@ class SortTable:
'cotis_id': ['id'], 'cotis_id': ['id'],
'default': ['-date'] 'default': ['-date']
} }
COTISATIONS_CUSTOM = {
'invoice_date': ['date'],
'invoice_id': ['id'],
'invoice_recipient': ['recipient'],
'invoice_address': ['address'],
'invoice_payment': ['payment'],
'default': ['-date']
}
COTISATIONS_CONTROL = { COTISATIONS_CONTROL = {
'control_name': ['user__adherent__name'], 'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'], 'control_surname': ['user__surname'],

View file

@ -21,6 +21,6 @@ You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc., with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
<a class="btn btn-primary btn-sm" role="button" href="{% url href id %}" title="{{ desc|default:"Ajouter" }}"> <a class="btn btn-primary btn-sm" role="button" href="{% if id %}{% url href id %}{% else %}{% url href %}{% endif %}" title="{{ desc|default:"Ajouter" }}">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
</a> </a>