diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec2da1a..fd4e0177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,3 +136,17 @@ Fix several issues with email accounts, you need to collect the static files. ```bash ./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 +``` diff --git a/cotisations/admin.py b/cotisations/admin.py index 587bc066..afe4621c 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,6 +30,7 @@ from django.contrib import admin from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente +from .models import CustomInvoice class FactureAdmin(VersionAdmin): @@ -37,6 +38,11 @@ class FactureAdmin(VersionAdmin): pass +class CustomInvoiceAdmin(VersionAdmin): + """Admin class for custom invoices.""" + pass + + class VenteAdmin(VersionAdmin): """Class admin d'une vente, tous les champs (facture related)""" pass @@ -69,3 +75,4 @@ admin.site.register(Banque, BanqueAdmin) admin.site.register(Paiement, PaiementAdmin) admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) +admin.site.register(CustomInvoice, CustomInvoiceAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 7ad9e413..ccf9d5d6 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -46,7 +46,7 @@ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin 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 @@ -131,24 +131,13 @@ class SelectClubArticleForm(Form): self.fields['article'].queryset = Article.find_allowed_articles(user) -# TODO : change Facture to Invoice -class NewFactureFormPdf(Form): +class CustomInvoiceForm(FormRevMixin, ModelForm): """ - Form used to create a custom PDF invoice. + Form used to create a custom invoice. """ - paid = forms.BooleanField(label=_l("Paid"), required=False) - # TODO : change dest field to recipient - dest = forms.CharField( - required=True, - max_length=255, - label=_l("Recipient") - ) - # TODO : change chambre field to address - chambre = forms.CharField( - required=False, - max_length=10, - label=_l("Address") - ) + class Meta: + model = CustomInvoice + fields = '__all__' class ArticleForm(FormRevMixin, ModelForm): diff --git a/cotisations/locale/fr/LC_MESSAGES/django.mo b/cotisations/locale/fr/LC_MESSAGES/django.mo index 6f295ccb..046a9b9b 100644 Binary files a/cotisations/locale/fr/LC_MESSAGES/django.mo and b/cotisations/locale/fr/LC_MESSAGES/django.mo differ diff --git a/cotisations/locale/fr/LC_MESSAGES/django.po b/cotisations/locale/fr/LC_MESSAGES/django.po index f13cc8d4..f9aee64d 100644 --- a/cotisations/locale/fr/LC_MESSAGES/django.po +++ b/cotisations/locale/fr/LC_MESSAGES/django.po @@ -21,9 +21,9 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\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" -"Last-Translator: Maël Kervella \n" +"Last-Translator: Hugo Levy-Falk \n" "Language-Team: \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" @@ -34,74 +34,39 @@ msgstr "" msgid "You don't have the rights to see this application." msgstr "Vous n'avez pas les droits de voir cette application." -#: forms.py:63 forms.py:321 -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 +#: forms.py:63 forms.py:274 msgid "Select a payment method" msgstr "Sélectionnez un moyen de paiement" -#: forms.py:83 forms.py:347 -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 +#: forms.py:66 msgid "Member" msgstr "Adhérent" -#: forms.py:186 +#: forms.py:68 msgid "Select the proprietary member" msgstr "Sélectionnez l'adhérent propriétaire" -#: forms.py:187 +#: forms.py:69 msgid "Validated invoice" 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" msgstr "Nom de l'article" -#: forms.py:239 +#: forms.py:192 msgid "Payment method name" msgstr "Nom du moyen de paiement" -#: forms.py:240 -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 +#: forms.py:230 msgid "Bank name" msgstr "Nom de la banque" -#: forms.py:380 -#, 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 +#: forms.py:287 #, python-format msgid "" "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 " "%(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." 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." msgstr "Vous n'avez pas le droit de modifier les facture de cette utilisateur." -#: models.py:172 +#: models.py:182 msgid "" "You don't have the right to edit an invoice already controlled or " "invalidated." @@ -126,15 +91,15 @@ msgstr "" "Vous n'avez pas le droit de modifier une facture précedement controllée ou " "invalidée." -#: models.py:179 +#: models.py:189 msgid "You don't have the right to delete an invoice." 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." msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur." -#: models.py:184 +#: models.py:194 msgid "" "You don't have the right to delete an invoice already controlled or " "invalidated." @@ -142,35 +107,41 @@ msgstr "" "Vous n'avez pas le droit de supprimer une facture précedement controllée ou " "invalidée." -#: models.py:192 +#: models.py:202 msgid "You don't have the right to see someone else's invoices history." msgstr "" "Vous n'avez pas le droit de voir l'historique de la facture de quelqu'un " "d'autre." -#: models.py:195 +#: models.py:205 msgid "The invoice has been invalidated." msgstr "La facture a été invalidée." -#: models.py:205 -#, fuzzy -#| msgid "You don't have the right to edit the controlled state." +#: models.py:215 msgid "You don't have the right to edit the \"controlled\" state." msgstr "Vous n'avez pas le droit de modifier l'état \"controllé\"." -#: models.py: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." 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." 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." msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur." -#: models.py:388 +#: models.py:440 msgid "" "You don't have the right to edit a purchase already controlled or " "invalidated." @@ -178,15 +149,15 @@ msgstr "" "Vous n'avez pas le droit de modifier un achat précédement controllé ou " "invalidé." -#: models.py:395 +#: models.py:447 msgid "You don't have the right to delete a purchase." 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." msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur." -#: models.py:400 +#: models.py:452 msgid "" "You don't have the right to delete a purchase already controlled or " "invalidated." @@ -194,29 +165,46 @@ msgstr "" "Vous n'avez pas le droit de supprimer un achat précédement controllé ou " "invalidé." -#: models.py:408 +#: models.py:460 msgid "You don't have the right to see someone else's purchase history." msgstr "" "Vous n'avez pas le droit de voir l'historique d'un achat de quelqu'un " "d'autre." -#: models.py:517 +#: models.py:582 msgid "Solde is a reserved article name" msgstr "Solde est un nom d'article réservé" -#: models.py:521 +#: models.py:586 msgid "Duration must be specified for a cotisation" msgstr "La durée doit être spécifiée pour une cotisation" -#: models.py:603 -msgid "You cannot have multiple payment method of type cheque" -msgstr "Vous ne pouvez avoir plusieurs moyens de paiement de type chèque" +#: models.py:607 +msgid "You cannot buy this Article." +msgstr "Vous ne pouvez pas acheter cet article." + +#: 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:654 +#: models.py:811 msgid "You don't have the right to edit a cotisation." msgstr "Vous n'avez pas le droit de modifier une cotisation." -#: models.py:658 +#: models.py:815 msgid "" "You don't have the right to edit a cotisation already controlled or " "invalidated." @@ -224,11 +212,11 @@ msgstr "" "Vous n'avez pas le droit de modifier une cotisaiton précédement controllée " "ou invalidée." -#: models.py:665 +#: models.py:822 msgid "You don't have the right to delete a cotisation." msgstr "Vous n'avez pas le droit de supprimer une cotisation." -#: models.py:668 +#: models.py:825 msgid "" "You don't have the right to delete a cotisation already controlled or " "invalidated." @@ -236,113 +224,159 @@ msgstr "" "Vous n'avez pas le droit de supprimer une cotisation précédement controllée " "ou invalidée." -#: models.py:676 +#: models.py:833 msgid "You don't have the right to see someone else's cotisation history." msgstr "" "Vous n'avez pas le droit de voir l'historique d'une cotisation de quelqu'un " "d'autre." -#: payment.py: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 msgid "The payment of %(amount)s € has been accepted." msgstr "Le paiement de %(amount)s € a été accepté." -#: payment.py:49 +#: payment_methods/comnpay/views.py:84 msgid "The payment has been refused." msgstr "Le paiment a été refusé." -#: templates/cotisations/aff_article.html:31 -#: templates/cotisations/facture.html:43 -#: templates/cotisations/new_facture.html:50 -#: templates/cotisations/new_facture_solde.html:44 +#: templates/cotisations/aff_article.html:33 +#: templates/cotisations/facture.html:60 msgid "Article" msgstr "Article" -#: templates/cotisations/aff_article.html:32 +#: templates/cotisations/aff_article.html:34 msgid "Price" msgstr "Prix" -#: templates/cotisations/aff_article.html:33 +#: templates/cotisations/aff_article.html:35 msgid "Cotisation type" msgstr "Type de cotisation" -#: templates/cotisations/aff_article.html:34 +#: templates/cotisations/aff_article.html:36 msgid "Duration (month)" msgstr "Durée (mois)" -#: templates/cotisations/aff_article.html:35 +#: templates/cotisations/aff_article.html:37 msgid "Concerned users" msgstr "Utilisateurs concernés" -#: templates/cotisations/aff_article.html:48 -#: templates/cotisations/aff_banque.html:40 -#: templates/cotisations/aff_cotisations.html:69 -#: templates/cotisations/aff_cotisations.html:75 -#: templates/cotisations/aff_paiement.html:40 -#: templates/cotisations/control.html:104 views.py:396 views.py:443 -#: views.py:507 views.py:585 -msgid "Edit" -msgstr "Modifier" +#: templates/cotisations/aff_article.html:38 +msgid "Available for everyone" +msgstr "Articles disponibles" #: templates/cotisations/aff_article.html:52 -#: templates/cotisations/aff_banque.html:44 -#: templates/cotisations/aff_cotisations.html:90 -#: templates/cotisations/aff_paiement.html:44 -msgid "Historique" -msgstr "Historique" +#: templates/cotisations/aff_banque.html:41 +#: 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" +msgstr "Modifier" -#: templates/cotisations/aff_banque.html:31 +#: templates/cotisations/aff_banque.html:32 msgid "Bank" msgstr "Banque" -#: templates/cotisations/aff_cotisations.html:37 +#: templates/cotisations/aff_cotisations.html:38 msgid "User" 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/edit_facture.html:45 msgid "Designation" 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 msgid "Total price" msgstr "Prix total" -#: templates/cotisations/aff_cotisations.html:43 -#: templates/cotisations/aff_paiement.html:31 +#: templates/cotisations/aff_cotisations.html:44 +#: templates/cotisations/aff_custom_invoice.html:45 #: templates/cotisations/control.html:63 msgid "Payment method" 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 msgid "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 msgid "Invoice id" msgstr "Id facture" -#: templates/cotisations/aff_cotisations.html:79 +#: templates/cotisations/aff_cotisations.html:80 msgid "Controlled invoice" msgstr "Facture controllé" -#: templates/cotisations/aff_cotisations.html:84 views.py:464 views.py:542 -#: views.py:620 +#: templates/cotisations/aff_cotisations.html:85 +#: templates/cotisations/aff_custom_invoice.html:86 views.py:502 views.py:604 +#: views.py:685 msgid "Delete" msgstr "Supprimer" -#: templates/cotisations/aff_cotisations.html:99 +#: templates/cotisations/aff_cotisations.html:98 +#: templates/cotisations/aff_custom_invoice.html:98 msgid "PDF" msgstr "PDF" -#: templates/cotisations/aff_cotisations.html:102 +#: templates/cotisations/aff_cotisations.html:101 msgid "Invalidated invoice" 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 msgid "Invoice control" msgstr "Contrôle des factures" @@ -394,15 +428,11 @@ msgstr "" #: templates/cotisations/delete.html:40 #: templates/cotisations/edit_facture.html:60 -#: templates/cotisations/new_facture_solde.html:59 -#: templates/cotisations/recharge.html:42 msgid "Confirm" msgstr "Confirmer" #: templates/cotisations/edit_facture.html:31 #: templates/cotisations/facture.html:30 -#: templates/cotisations/new_facture.html:30 -#: templates/cotisations/new_facture_solde.html:30 msgid "Invoices creation and edition" msgstr "Création et modification de factures" @@ -411,9 +441,7 @@ msgid "Edit the invoice" msgstr "Edition de factures" #: templates/cotisations/edit_facture.html:41 -#: templates/cotisations/facture.html:38 -#: templates/cotisations/new_facture.html:46 -#: templates/cotisations/new_facture_solde.html:40 +#: templates/cotisations/facture.html:55 msgid "Invoice's articles" msgstr "Articles de la facture" @@ -421,15 +449,23 @@ msgstr "Articles de la facture" msgid "Quantity" msgstr "Quantité" -#: templates/cotisations/facture.html:52 -#: templates/cotisations/new_facture.html:59 -#: templates/cotisations/new_facture_solde.html:53 +#: templates/cotisations/facture.html:36 +msgid "New invoice" +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" msgstr "Ajouter un article" -#: templates/cotisations/facture.html:54 -#: templates/cotisations/new_facture.html:61 -#: templates/cotisations/new_facture_solde.html:55 +#: templates/cotisations/facture.html:71 msgid "" "\n" " Total price : 0,00 €\n" @@ -464,7 +500,7 @@ msgid "Delete article types" msgstr "Supprimer des types d'articles" #: templates/cotisations/index_banque.html:30 -#: templates/cotisations/sidebar.html:50 +#: templates/cotisations/sidebar.html:55 msgid "Banks" msgstr "Banques" @@ -480,6 +516,15 @@ msgstr "Ajouter une banque" msgid "Delete banks" 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 msgid "Payments" msgstr "Paiement" @@ -496,59 +541,24 @@ msgstr "Ajouter un type de paiement" msgid "Delete payment types" msgstr "Supprimer un type de paiement" -#: templates/cotisations/new_facture.html:37 -#: templates/cotisations/new_facture_solde.html:37 -msgid "New invoice" -msgstr "Nouvelle facture" - -#: templates/cotisations/new_facture.html:39 -#, python-format -msgid "" -"\n" -" User's balance : %(user.solde)s €\n" -" " -msgstr "" -"\n" -" Solde de l'utilisateur : %(user.solde)s €\n" -" " - -#: templates/cotisations/new_facture.html:65 views.py:257 -msgid "Create" -msgstr "Créer" - -#: templates/cotisations/payment.html:30 templates/cotisations/recharge.html:30 -#: templates/cotisations/recharge.html:33 +#: templates/cotisations/payment.html:30 msgid "Balance refill" msgstr "Rechargement de solde" #: templates/cotisations/payment.html:34 -#, python-format msgid "" "\n" -" Refill of %(amount)s €\n" +" Pay %(amount)s €\n" " " msgstr "" "\n" " Recharger de %(amount)s €\n" " " -#: templates/cotisations/payment.html:40 +#: templates/cotisations/payment.html:44 views.py:867 msgid "Pay" msgstr "Payer" -#: templates/cotisations/recharge.html:35 -#, python-format -msgid "" -"\n" -" Balance : %(request.user.solde)s " -"€\n" -" " -msgstr "" -"\n" -" Solde : %(request.user.solde)s " -"€\n" -" " - #: templates/cotisations/sidebar.html:32 msgid "Create an invoice" msgstr "Créer une facture" @@ -557,83 +567,94 @@ msgstr "Créer une facture" msgid "Control the invoices" msgstr "Contrôler les factures" -#: templates/cotisations/sidebar.html:45 +#: templates/cotisations/sidebar.html:50 msgid "Available articles" msgstr "Articles disponibles" -#: templates/cotisations/sidebar.html:55 +#: templates/cotisations/sidebar.html:60 msgid "Payment methods" msgstr "Moyens de paiement" -#: views.py:138 -msgid "Your balance is too low for this operation." -msgstr "Votre solde est trop faible pour cette opération." - -#: views.py:168 -#, 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." +#: validators.py:20 +msgid "There are already payment method(s) for user balance" +msgstr "Il y a déjà une méthode de paiement pour le solde utilisateur" -#: views.py:186 views.py:824 +#: views.py:165 msgid "You need to choose at least one 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." 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." msgstr "La facture a été supprimée avec succès." -#: views.py:363 +#: views.py:338 views.py:432 msgid "Invoice" msgstr "Facture" -#: views.py:391 -msgid "Balance successfully updated." -msgstr "Solde mis à jour avec succès." - -#: views.py:417 +#: views.py:453 msgid "The article has been successfully created." msgstr "L'article a été créé avec succès." -#: views.py:422 views.py:485 views.py:563 -#, fuzzy -#| msgid "Address" -msgid "Add" +#: views.py:458 views.py:531 views.py:626 +msgid "Address" msgstr "Adresse" -#: views.py:438 +#: views.py:459 +msgid "New article" +msgstr "Nouvel article" + +#: views.py:475 msgid "The article has been successfully edited." 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." 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." 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." 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 msgid "" "The payment method %(method_name)s has been successfully " "deleted." msgstr "Le moyen de paiement %(method_name)s a été supprimé avec succès." -#: views.py:534 +#: views.py:596 #, python-format msgid "" "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 " "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." 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" 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 msgid "" "The bank %(bank_name)s has been successfully deleted." msgstr "La banque %(bank_name)s a été supprimée avec succès." -#: views.py:612 +#: views.py:677 #, python-format msgid "" "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 " "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." msgstr "Vos modifications ont correctement été prises en compte." -#: views.py:776 -msgid "The balance is too low for this operation." -msgstr "Le solde est trop faible pour cette opération." +#: views.py:834 +msgid "You are not allowed to credit your balance." +msgstr "Vous n'êtes pas autorisé à créditer votre solde." -#: views.py:806 -#, python-format -msgid "" -"The cotisation of %(member_name)s has been successfully " -"extended to %(end_date)s." -msgstr "La cotisation de %(member_name)s a été prolongée jusqu'à %(end_date)s." - -#: views.py:816 -msgid "The invoice has been successuflly created." -msgstr "La facture a été créée avec succès." - -#: views.py:846 -msgid "Online payment is disabled." -msgstr "Le paiement en ligne est désactivé." - -#~ msgid "Paid" -#~ msgstr "Payé" - -#~ msgid "Recipient" -#~ msgstr "Destinataire" - -#~ msgid "Invoice number" -#~ msgstr "Numéro de facture" - -#~ msgid "Existing articles" -#~ msgstr "Articles disponibles" +#: views.py:866 +msgid "Refill your balance" +msgstr "Créditer votre solde" -#~ msgid "Existing payment method" -#~ msgstr "Moyen de paiements disponibles" - -#~ msgid "Existing banks" -#~ msgstr "Banques disponibles" +#: models.py:137 +msgid "Cheque number" +msgstr "Numéro de chèque" -#~ msgid "Amount" -#~ msgstr "Montant" +msgid "Not specified" +msgstr "Non renseigné" -#~ msgid "Can change the \"controlled\" state" -#~ msgstr "Peut modifier l'état \"controllé\"" +msgid "A cheque number and a bank must be specified." +msgstr "Un numéro de chèqe et une banque doivent être renseignés." -#~ msgid "Can create a custom PDF invoice" -#~ msgstr "Peut crée une facture PDF personnalisée" +#: models.py:155 +msgid "Can change the \"controlled\" state" +msgstr "Peut modifier l'état \"controllé\"" -#~ msgid "Can see an invoice's details" -#~ msgstr "Peut voir les détails d'une facture" +#: models.py:157 +msgid "Can see an invoice's details" +msgstr "Peut voir les détails d'une facture" -#~ msgid "Can edit all the previous invoices" -#~ msgstr "Peut modifier toutes les factures existantes" +#: models.py:159 +msgid "Can edit all the previous invoices" +msgstr "Peut modifier toutes les factures existantes" -#~ msgid "Connexion" -#~ msgstr "Connexion" +#: models.py:297 +msgid "Connexion" +msgstr "Connexion" -#~ msgid "Membership" -#~ msgstr "Adhésion" +#: models.py:336 +msgid "Membership" +msgstr "Adhésion" -#~ msgid "Both of them" -#~ msgstr "Les deux" +#: models.py:299 +msgid "Both of them" +msgstr "Les deux" -#~ msgid "Duration (in whole month)" -#~ msgstr "Durée (en mois entiers)" +#: models.py:328 +msgid "Duration (in whole month)" +msgstr "Durée (en mois entiers)" -#~ msgid "Type of cotisation" -#~ msgstr "Type de cotisation" +#: models.py:336 +msgid "Type of cotisation" +msgstr "Type de cotisation" -#~ msgid "Can see a purchase's details" -#~ msgstr "Peut voir les détails d'un achat" +#: models.py:341 +msgid "Can see a purchase's details" +msgstr "Peut voir les détails d'un achat" -#~ msgid "Can edit all the previous purchases" -#~ msgstr "Peut voir les achats existants" +#: models.py:342 +msgid "Can edit all the previous purchases" +msgstr "Peut voir les achats existants" -#~ msgid "Purchase" -#~ msgstr "Achat" +#: models.py:344 +msgid "Purchase" +msgstr "Achat" -#~ msgid "Purchases" -#~ msgstr "Achat" +#: models.py:345 +msgid "Purchases" +msgstr "Achat" -#~ msgid "Club" -#~ msgstr "Club" +#: models.py:512 +msgid "Club" +msgstr "Club" -#~ msgid "Unitary price" -#~ msgstr "Prix unitaire" +#: models.py:530 +msgid "Unitary price" +msgstr "Prix unitaire" -#~ msgid "Type of users concerned" -#~ msgstr "Type d'utilisateurs concernés" +#: models.py:538 +msgid "Type of users concerned" +msgstr "Type d'utilisateurs concernés" -#~ msgid "Can see an article's details" -#~ msgstr "Peut voir les détails d'un article" +#: models.py:561 +msgid "Can see an article's details" +msgstr "Peut voir les détails d'un article" -#~ msgid "Name" -#~ msgstr "Nom" +#: models.py:621 +msgid "Name" +msgstr "Nom" -#~ msgid "Can see a bank's details" -#~ msgstr "Peut voir les détails d'une banque" +#: models.py:626 +msgid "Can see a bank's details" +msgstr "Peut voir les détails d'une banque" -#~ msgid "Standard" -#~ msgstr "Standard" +#: models.py:344 +msgid "Standard" +msgstr "Standard" -#~ msgid "Cheque" -#~ msgstr "Chèque" +msgid "Cheque" +msgstr "Chèque" -#~ msgid "Method" -#~ msgstr "Moyen" +#: models.py:647 +msgid "Method" +msgstr "Moyen" -#~ msgid "Can see a payement's details" -#~ msgstr "Peut voir les détails d'un paiement" +#: models.py:663 +msgid "Can see a payement's details" +msgstr "Peut voir les détails d'un paiement" -#~ msgid "Starting date" -#~ msgstr "Date de début" +#: models.py:785 +msgid "Starting date" +msgstr "Date de début" -#~ msgid "Ending date" -#~ msgstr "Date de fin" +#: models.py:788 +msgid "Ending date" +msgstr "Date de fin" -#~ msgid "Can see a cotisation's details" -#~ msgstr "Peut voir les détails d'une cotisation" +#: models.py:793 +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" +#: models.py:794 +msgid "Can edit the previous cotisations" +msgstr "Peut voir les cotisations existantes" diff --git a/cotisations/migrations/0032_custom_invoice.py b/cotisations/migrations/0032_custom_invoice.py new file mode 100644 index 00000000..e143ae13 --- /dev/null +++ b/cotisations/migrations/0032_custom_invoice.py @@ -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'}, + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 52d71b58..7ca157eb 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -55,8 +55,52 @@ from cotisations.utils import find_payment_method from cotisations.validators import check_no_balance +class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): + date = models.DateTimeField( + auto_now_add=True, + verbose_name=_l("Date") + ) + + # TODO : change prix to price + def prix(self): + """ + 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 price + + # TODO : change prix to price + def prix_total(self): + """ + 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 + ).aggregate( + total=models.Sum( + models.F('prix')*models.F('number'), + output_field=models.FloatField() + ) + )['total'] or 0 + + def name(self): + """ + Returns : a string with the name of all the articles in the invoice. + Used for reprensenting the invoice with a string. + """ + name = ' - '.join(Vente.objects.filter( + facture=self + ).values_list('name', flat=True)) + return name + + # TODO : change facture to invoice -class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): +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). @@ -92,10 +136,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): blank=True, verbose_name=_l("Cheque number") ) - date = models.DateTimeField( - auto_now_add=True, - verbose_name=_l("Date") - ) # TODO : change name to validity for clarity valid = models.BooleanField( default=True, @@ -113,10 +153,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): # 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', @@ -130,43 +166,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): Usefull in history display""" return self.vente_set.all() - # TODO : change prix to price - def prix(self): - """ - 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 price - - # TODO : change prix to price - def prix_total(self): - """ - 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 - ).aggregate( - total=models.Sum( - models.F('prix')*models.F('number'), - output_field=models.FloatField() - ) - )['total'] or 0 - - def name(self): - """ - Returns : a string with the name of all the articles in the invoice. - Used for reprensenting the invoice with a string. - """ - name = ' - '.join(Vente.objects.filter( - facture=self - ).values_list('name', flat=True)) - return name - def can_edit(self, user_request, *args, **kwargs): if not user_request.has_perm('cotisations.change_facture'): 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.") ) - @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 def can_create(user_request, *_args, **_kwargs): """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) +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 class Vente(RevMixin, AclMixin, models.Model): """ @@ -288,7 +301,7 @@ class Vente(RevMixin, AclMixin, models.Model): # TODO : change facture to invoice facture = models.ForeignKey( - 'Facture', + 'BaseInvoice', on_delete=models.CASCADE, verbose_name=_l("Invoice") ) @@ -355,6 +368,10 @@ class Vente(RevMixin, AclMixin, models.Model): cotisation_type defined (which means the article sold represents a cotisation) """ + try: + invoice = self.facture.facture + except Facture.DoesNotExist: + return if not hasattr(self, 'cotisation') and self.type_cotisation: cotisation = Cotisation(vente=self) cotisation.type_cotisation = self.type_cotisation @@ -362,7 +379,7 @@ class Vente(RevMixin, AclMixin, models.Model): end_cotisation = Cotisation.objects.filter( vente__in=Vente.objects.filter( facture__in=Facture.objects.filter( - user=self.facture.user + user=invoice.user ).exclude(valid=False)) ).filter( Q(type_cotisation='All') | @@ -371,9 +388,9 @@ class Vente(RevMixin, AclMixin, models.Model): date_start__lt=date_start ).aggregate(Max('date_end'))['date_end__max'] elif self.type_cotisation == "Adhesion": - end_cotisation = self.facture.user.end_adhesion() + end_cotisation = invoice.user.end_adhesion() else: - end_cotisation = self.facture.user.end_connexion() + end_cotisation = invoice.user.end_connexion() date_start = date_start or timezone.now() end_cotisation = end_cotisation or 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. """ purchase = kwargs['instance'] + try: + purchase.facture.facture + except Facture.DoesNotExist: + return if hasattr(purchase, 'cotisation'): purchase.cotisation.vente = purchase purchase.cotisation.save() @@ -462,8 +483,12 @@ def vente_post_delete(**kwargs): Synchronise the LDAP user after a purchase has been deleted. """ purchase = kwargs['instance'] + try: + invoice = purchase.facture.facture + except Facture.DoesNotExist: + return if purchase.type_cotisation: - user = purchase.facture.user + user = invoice.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index 558dd7a2..f547bcf0 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -46,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ article.type_cotisation }} {{ article.duration }} {{ article.type_user }} - {{ article.available_for_everyone|tick }} + {{ article.available_for_everyone | tick }} {% can_edit article %} diff --git a/cotisations/templates/cotisations/aff_custom_invoice.html b/cotisations/templates/cotisations/aff_custom_invoice.html new file mode 100644 index 00000000..1d182178 --- /dev/null +++ b/cotisations/templates/cotisations/aff_custom_invoice.html @@ -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 %} + +
+ {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} + + + + + + + + + + + + + + + + {% for invoice in custom_invoice_list %} + + + + + + + + + + + {% endfor %} +
+ {% trans "Recipient" as tr_recip %} + {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %} + {% trans "Designation" %}{% trans "Total price" %} + {% trans "Payment method" as tr_payment_method %} + {% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %} + + {% trans "Date" as tr_date %} + {% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %} + + {% trans "Invoice id" as tr_invoice_id %} + {% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %} + + {% trans "Paid" as tr_invoice_paid%} + {% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_paid %} +
{{ invoice.recipient }}{{ invoice.name }}{{ invoice.prix_total }}{{ invoice.payment }}{{ invoice.date }}{{ invoice.id }}{{ invoice.paid|tick }} + {% 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 %} + + {% trans "PDF" %} + +
+ + {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} +
diff --git a/cotisations/templates/cotisations/index_custom_invoice.html b/cotisations/templates/cotisations/index_custom_invoice.html new file mode 100644 index 00000000..67d00126 --- /dev/null +++ b/cotisations/templates/cotisations/index_custom_invoice.html @@ -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 %} +

{% trans "Custom invoices list" %}

+{% 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 %} diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 296730f2..7be91b1c 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -27,8 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load i18n %} {% block sidebar %} - {% can_change Facture pdf %} -
+ {% can_create CustomInvoice %} + {% trans "Create an invoice" %} @@ -40,6 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Invoices" %} {% acl_end %} + {% can_view_all CustomInvoice %} + + {% trans "Custom invoices" %} + + {% acl_end %} {% can_view_all Article %} {% trans "Available articles" %} diff --git a/cotisations/tex.py b/cotisations/tex.py index f3f8601b..b7e1cb81 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -105,7 +105,7 @@ def render_tex(_request, template, ctx={}): Returns: 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.write(pdf) return r diff --git a/cotisations/urls.py b/cotisations/urls.py index 470ccbfa..edc448fe 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -52,9 +52,29 @@ urlpatterns = [ name='facture-pdf' ), url( - r'^new_facture_pdf/$', - views.new_facture_pdf, - name='new-facture-pdf' + r'^index_custom_invoice/$', + views.index_custom_invoice, + name='index-custom-invoice' + ), + url( + r'^new_custom_invoice/$', + views.new_custom_invoice, + name='new-custom-invoice' + ), + url( + r'^edit_custom_invoice/(?P[0-9]+)$', + views.edit_custom_invoice, + name='edit-custom-invoice' + ), + url( + r'^custom_invoice_pdf/(?P[0-9]+)$', + views.custom_invoice_pdf, + name='custom-invoice-pdf', + ), + url( + r'^del_custom_invoice/(?P[0-9]+)$', + views.del_custom_invoice, + name='del-custom-invoice' ), url( r'^credit_solde/(?P[0-9]+)$', diff --git a/cotisations/views.py b/cotisations/views.py index 66eb66f5..90bc3632 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -58,7 +58,15 @@ from re2o.acl import ( can_change, ) 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 ( FactureForm, ArticleForm, @@ -67,10 +75,10 @@ from .forms import ( DelPaiementForm, BanqueForm, DelBanqueForm, - NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - RechargeForm + RechargeForm, + CustomInvoiceForm ) from .tex import render_invoice from .payment_methods.forms import payment_method_factory @@ -178,10 +186,10 @@ def new_facture(request, user, userid): # TODO : change facture to invoice @login_required -@can_change(Facture, 'pdf') -def new_facture_pdf(request): +@can_create(CustomInvoice) +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 point of view. """ @@ -190,7 +198,7 @@ def new_facture_pdf(request): Q(type_user='All') | Q(type_user=request.user.class_name) ) # 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: articles_formset = formset_factory(SelectClubArticleForm)( request.POST or None, @@ -202,44 +210,31 @@ def new_facture_pdf(request): form_kwargs={'user': request.user} ) if invoice_form.is_valid() and articles_formset.is_valid(): - # Get the article list and build an list out of it - # contiaining (article_name, article_price, quantity, total_price) - articles_info = [] - for articles_form in articles_formset: - if articles_form.cleaned_data: - article = articles_form.cleaned_data['article'] - quantity = articles_form.cleaned_data['quantity'] - articles_info.append({ - 'name': article.name, - 'price': article.prix, - 'quantity': quantity, - 'total_price': article.prix * quantity - }) - paid = invoice_form.cleaned_data['paid'] - recipient = invoice_form.cleaned_data['dest'] - address = invoice_form.cleaned_data['chambre'] - total_price = sum(a['total_price'] for a in articles_info) - - 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) - }) + new_invoice_instance = invoice_form.save() + for art_item in articles_formset: + if art_item.cleaned_data: + article = art_item.cleaned_data['article'] + quantity = art_item.cleaned_data['quantity'] + Vente.objects.create( + facture=new_invoice_instance, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + messages.success( + request, + _('The custom invoice was successfully created.') + ) + return redirect(reverse('cotisations:index-custom-invoice')) + + return form({ 'factureform': invoice_form, 'action_name': _("Create"), 'articlesformset': articles_formset, - 'articles': articles + 'articlelist': articles }, 'cotisations/facture.html', request) @@ -292,7 +287,7 @@ def facture_pdf(request, facture, **_kwargs): def edit_facture(request, facture, **_kwargs): """ 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 an invoice. """ @@ -347,6 +342,100 @@ def del_facture(request, facture, **_kwargs): }, '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 @can_create(Article) 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 @can_view_all(Facture) +@can_view_all(CustomInvoice) def index(request): """ 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) return render(request, 'cotisations/index.html', { - 'facture_list': invoice_list + 'facture_list': invoice_list, }) diff --git a/re2o/utils.py b/re2o/utils.py index 75304369..6f7870f0 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -250,6 +250,14 @@ class SortTable: 'cotis_id': ['id'], 'default': ['-date'] } + COTISATIONS_CUSTOM = { + 'invoice_date': ['date'], + 'invoice_id': ['id'], + 'invoice_recipient': ['recipient'], + 'invoice_address': ['address'], + 'invoice_payment': ['payment'], + 'default': ['-date'] + } COTISATIONS_CONTROL = { 'control_name': ['user__adherent__name'], 'control_surname': ['user__surname'], diff --git a/templates/buttons/add.html b/templates/buttons/add.html index 17058b89..33148a7b 100644 --- a/templates/buttons/add.html +++ b/templates/buttons/add.html @@ -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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - +