From e7a7e81a2c9916a624b3aff11935677e9b5aae80 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 14:01:22 +0100 Subject: [PATCH 1/5] Add discount for custom invoices. --- cotisations/forms.py | 45 +++- .../templates/cotisations/facture.html | 196 ++++++++++-------- cotisations/tex.py | 15 ++ cotisations/views.py | 14 +- 4 files changed, 172 insertions(+), 98 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 01e52756..73cb4971 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, CustomInvoice +from .models import Article, Paiement, Facture, Banque, CustomInvoice, Vente from .payment_methods import balance @@ -104,7 +104,44 @@ class SelectArticleForm(FormRevMixin, Form): user = kwargs.pop('user') target_user = kwargs.pop('target_user', None) super(SelectArticleForm, self).__init__(*args, **kwargs) - self.fields['article'].queryset = Article.find_allowed_articles(user, target_user) + self.fields['article'].queryset = Article.find_allowed_articles( + user, target_user) + + +class DiscountForm(Form): + """ + Form used in oder to create a discount on an invoice. + """ + is_relative = forms.BooleanField( + label=_("Discount is on percentage"), + required=False, + ) + discount = forms.DecimalField( + label=_("Discount"), + max_value=100, + min_value=0, + max_digits=5, + decimal_places=2, + required=False, + ) + + def apply_to_invoice(self, invoice): + invoice_price = invoice.prix_total() + discount = self.cleaned_data['discount'] + is_relative = self.cleaned_data['is_relative'] + if is_relative: + amount = discount/100 * invoice_price + else: + amount = discount + if amount > 0: + name = _("{}% discount") if is_relative else _("{}€ discount") + name = name.format(discount) + Vente.objects.create( + facture=invoice, + name=name, + prix=-amount, + number=1 + ) class CustomInvoiceForm(FormRevMixin, ModelForm): @@ -248,7 +285,8 @@ class RechargeForm(FormRevMixin, Form): super(RechargeForm, self).__init__(*args, **kwargs) self.fields['payment'].empty_label = \ _("Select a payment method") - self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True) + self.fields['payment'].queryset = Paiement.find_allowed_payments( + user_source).exclude(is_balance=True) def clean(self): """ @@ -266,4 +304,3 @@ class RechargeForm(FormRevMixin, Form): } ) return self.cleaned_data - diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 1f87f579..ff9ed837 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -44,6 +44,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}

{% endif %} +{% bootstrap_form_errors factureform %} +{% bootstrap_form_errors discount_form %}
{% csrf_token %} @@ -68,8 +70,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %} +

{% trans "Discount" %}

+ {% if discount_form %} + {% bootstrap_form discount_form %} + {% endif %}

- {% blocktrans %}Total price: 0,00 €{% endblocktrans %} + {% blocktrans %}Total price: 0,00 €{% endblocktrans %}

{% endif %} {% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %} @@ -78,105 +84,117 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if articlesformset or payment_method%} {% endif %} diff --git a/cotisations/tex.py b/cotisations/tex.py index 3f404f22..d6c0ae5f 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -36,6 +36,7 @@ from django.template import Context from django.http import HttpResponse from django.conf import settings from django.utils.text import slugify +import logging TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') @@ -93,6 +94,20 @@ def create_pdf(template, ctx={}): return pdf +def escape_chars(string): + """Escape the '%' and the '€' signs to avoid messing with LaTeX""" + if not isinstance(string, str): + return string + mapping = ( + ('€', r'\euro'), + ('%', r'\%'), + ) + r = str(string) + for k, v in mapping: + r = r.replace(k, v) + return r + + def render_tex(_request, template, ctx={}): """Creates a PDF from a LaTex templates using pdflatex. diff --git a/cotisations/views.py b/cotisations/views.py index 68118711..7d4185cc 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -80,9 +80,10 @@ from .forms import ( DelBanqueForm, SelectArticleForm, RechargeForm, - CustomInvoiceForm + CustomInvoiceForm, + DiscountForm ) -from .tex import render_invoice +from .tex import render_invoice, escape_chars from .payment_methods.forms import payment_method_factory from .utils import find_payment_method @@ -198,8 +199,9 @@ def new_custom_invoice(request): request.POST or None, form_kwargs={'user': request.user} ) + discount_form = DiscountForm(request.POST or None) - if invoice_form.is_valid() and articles_formset.is_valid(): + if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid(): new_invoice_instance = invoice_form.save() for art_item in articles_formset: if art_item.cleaned_data: @@ -213,6 +215,7 @@ def new_custom_invoice(request): duration=article.duration, number=quantity ) + discount_form.apply_to_invoice(new_invoice_instance) messages.success( request, _("The custom invoice was created.") @@ -223,7 +226,8 @@ def new_custom_invoice(request): 'factureform': invoice_form, 'action_name': _("Confirm"), 'articlesformset': articles_formset, - 'articlelist': articles + 'articlelist': articles, + 'discount_form': discount_form }, 'cotisations/facture.html', request) @@ -382,7 +386,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs): purchases_info = [] for purchase in purchases_objects: purchases_info.append({ - 'name': purchase.name, + 'name': escape_chars(purchase.name), 'price': purchase.prix, 'quantity': purchase.number, 'total_price': purchase.prix_total From fd57a9b9250543763be85d34d0b640ed999ad158 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 15:15:33 +0100 Subject: [PATCH 2/5] Display payment method on invoices --- .../templates/cotisations/factures.tex | 24 ++++++++++++------- cotisations/views.py | 6 +++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 3f2ebedc..226682e7 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -43,7 +43,7 @@ \begin{document} - + %---------------------------------------------------------------------------------------- % HEADING SECTION %---------------------------------------------------------------------------------------- @@ -70,7 +70,7 @@ {\bf Siret :} {{siret|safe}} \vspace{2cm} - + \begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r} {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ @@ -84,20 +84,20 @@ %---------------------------------------------------------------------------------------- % TABLE OF EXPENSES %---------------------------------------------------------------------------------------- - + \begin{tabularx}{\textwidth}{|X|r|r|r|} \hline \textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\ \doublehline - + {% for a in article %} {{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\ \hline {% endfor %} - + \end{tabularx} - + \vspace{1cm} \hfill @@ -109,14 +109,22 @@ \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ \hline \end{tabular} - + + \vspace{1cm} + \begin{tabularx}{\textwidth}{c X} + \hline + \textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\ + \hline + \end{tabularx} + + \vfill %---------------------------------------------------------------------------------------- % FOOTNOTE %---------------------------------------------------------------------------------------- - + \hrule \smallskip \footnotesize{TVA non applicable, art. 293 B du CGI} diff --git a/cotisations/views.py b/cotisations/views.py index 7d4185cc..902db508 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -270,7 +270,8 @@ def facture_pdf(request, facture, **_kwargs): '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) + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': facture.paiement.moyen, }) @@ -405,7 +406,8 @@ def custom_invoice_pdf(request, invoice, **_kwargs): '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) + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': invoice.payment, }) From f612e4192f8ae5d4a5cf039d0453979e3553508c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 15:25:52 +0100 Subject: [PATCH 3/5] Add remark to custom invoices --- .../migrations/0036_custominvoice_remark.py | 20 +++++++++++++++++++ cotisations/models.py | 5 +++++ .../templates/cotisations/factures.tex | 6 +++++- cotisations/views.py | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 cotisations/migrations/0036_custominvoice_remark.py diff --git a/cotisations/migrations/0036_custominvoice_remark.py b/cotisations/migrations/0036_custominvoice_remark.py new file mode 100644 index 00000000..7719b31d --- /dev/null +++ b/cotisations/migrations/0036_custominvoice_remark.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-29 14:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0035_notepayment'), + ] + + operations = [ + migrations.AddField( + model_name='custominvoice', + name='remark', + field=models.TextField(blank=True, null=True, verbose_name='Remark'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index f8e53b50..979b444a 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -286,6 +286,11 @@ class CustomInvoice(BaseInvoice): paid = models.BooleanField( verbose_name=_("Paid") ) + remark = models.TextField( + verbose_name=_("Remark"), + blank=True, + null=True + ) # TODO : change Vente to Purchase diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 226682e7..11e490d7 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -111,10 +111,14 @@ \end{tabular} \vspace{1cm} - \begin{tabularx}{\textwidth}{c X} + \begin{tabularx}{\textwidth}{r X} \hline \textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\ \hline + {% if remark %} + \textbf{Remarque} & {{remark|safe}} \\ + \hline + {% endif %} \end{tabularx} diff --git a/cotisations/views.py b/cotisations/views.py index 902db508..ec746bb7 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -408,6 +408,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs): 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), 'payment_method': invoice.payment, + 'remark': invoice.remark, }) From b85384b226a67f040c78aa34850aebf16d37a0e3 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 29 Dec 2018 23:55:18 +0100 Subject: [PATCH 4/5] Do not fail on empty discount --- cotisations/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 73cb4971..56e90c58 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -133,7 +133,7 @@ class DiscountForm(Form): amount = discount/100 * invoice_price else: amount = discount - if amount > 0: + if amount: name = _("{}% discount") if is_relative else _("{}€ discount") name = name.format(discount) Vente.objects.create( From 37dbfd2fbf3d41590d12d08b9a50d00fbb7fcd27 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 31 Dec 2018 23:58:37 +0100 Subject: [PATCH 5/5] Add Cost Estimates --- cotisations/admin.py | 8 +- cotisations/forms.py | 14 +- cotisations/migrations/0037_costestimate.py | 28 +++ .../migrations/0038_auto_20181231_1657.py | 31 +++ cotisations/models.py | 54 ++++- .../cotisations/aff_cost_estimate.html | 101 +++++++++ .../templates/cotisations/edit_facture.html | 4 + .../templates/cotisations/factures.tex | 10 + .../cotisations/index_cost_estimate.html | 36 ++++ .../templates/cotisations/sidebar.html | 5 + cotisations/tex.py | 3 +- cotisations/urls.py | 30 +++ cotisations/views.py | 196 +++++++++++++++++- 13 files changed, 511 insertions(+), 9 deletions(-) create mode 100644 cotisations/migrations/0037_costestimate.py create mode 100644 cotisations/migrations/0038_auto_20181231_1657.py create mode 100644 cotisations/templates/cotisations/aff_cost_estimate.html create mode 100644 cotisations/templates/cotisations/index_cost_estimate.html diff --git a/cotisations/admin.py b/cotisations/admin.py index afe4621c..4b47ccc8 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,7 +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 +from .models import CustomInvoice, CostEstimate class FactureAdmin(VersionAdmin): @@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin): pass +class CostEstimateAdmin(VersionAdmin): + """Admin class for cost estimates.""" + pass + + class CustomInvoiceAdmin(VersionAdmin): """Admin class for custom invoices.""" pass @@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin) admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin) +admin.site.register(CostEstimate, CostEstimateAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 56e90c58..57bd7355 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -46,7 +46,10 @@ 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, CustomInvoice, Vente +from .models import ( + Article, Paiement, Facture, Banque, + CustomInvoice, Vente, CostEstimate +) from .payment_methods import balance @@ -153,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm): fields = '__all__' +class CostEstimateForm(FormRevMixin, ModelForm): + """ + Form used to create a cost estimate. + """ + class Meta: + model = CostEstimate + exclude = ['paid', 'final_invoice'] + + class ArticleForm(FormRevMixin, ModelForm): """ Form used to create an article. diff --git a/cotisations/migrations/0037_costestimate.py b/cotisations/migrations/0037_costestimate.py new file mode 100644 index 00000000..3d97f3f3 --- /dev/null +++ b/cotisations/migrations/0037_costestimate.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-29 21:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0036_custominvoice_remark'), + ] + + operations = [ + migrations.CreateModel( + name='CostEstimate', + fields=[ + ('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')), + ('validity', models.DurationField(verbose_name='Period of validity')), + ('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')), + ], + options={ + 'permissions': (('view_costestimate', 'Can view a cost estimate object'),), + }, + bases=('cotisations.custominvoice',), + ), + ] diff --git a/cotisations/migrations/0038_auto_20181231_1657.py b/cotisations/migrations/0038_auto_20181231_1657.py new file mode 100644 index 00000000..a9415bf0 --- /dev/null +++ b/cotisations/migrations/0038_auto_20181231_1657.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-31 22:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0037_costestimate'), + ] + + operations = [ + migrations.AlterField( + model_name='costestimate', + name='final_invoice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'), + ), + migrations.AlterField( + model_name='costestimate', + name='validity', + field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'), + ), + migrations.AlterField( + model_name='custominvoice', + name='paid', + field=models.BooleanField(default=False, verbose_name='Paid'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 979b444a..623db068 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -284,7 +284,8 @@ class CustomInvoice(BaseInvoice): verbose_name=_("Address") ) paid = models.BooleanField( - verbose_name=_("Paid") + verbose_name=_("Paid"), + default=False ) remark = models.TextField( verbose_name=_("Remark"), @@ -293,6 +294,57 @@ class CustomInvoice(BaseInvoice): ) +class CostEstimate(CustomInvoice): + class Meta: + permissions = ( + ('view_costestimate', _("Can view a cost estimate object")), + ) + validity = models.DurationField( + verbose_name=_("Period of validity"), + help_text="DD HH:MM:SS" + ) + final_invoice = models.ForeignKey( + CustomInvoice, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="origin_cost_estimate", + primary_key=False + ) + + def create_invoice(self): + """Create a CustomInvoice from the CostEstimate.""" + if self.final_invoice is not None: + return self.final_invoice + invoice = CustomInvoice() + invoice.recipient = self.recipient + invoice.payment = self.payment + invoice.address = self.address + invoice.paid = False + invoice.remark = self.remark + invoice.date = timezone.now() + invoice.save() + self.final_invoice = invoice + self.save() + for sale in self.vente_set.all(): + Vente.objects.create( + facture=invoice, + name=sale.name, + prix=sale.prix, + number=sale.number, + ) + return invoice + + def can_delete(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.delete_costestimate'): + return False, _("You don't have the right " + "to delete a cost estimate.") + if self.final_invoice is not None: + return False, _("The cost estimate has an " + "invoice and cannot be deleted.") + return True, None + + # TODO : change Vente to Purchase class Vente(RevMixin, AclMixin, models.Model): """ diff --git a/cotisations/templates/cotisations/aff_cost_estimate.html b/cotisations/templates/cotisations/aff_cost_estimate.html new file mode 100644 index 00000000..d4a3f60d --- /dev/null +++ b/cotisations/templates/cotisations/aff_cost_estimate.html @@ -0,0 +1,101 @@ +{% 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 cost_estimate_list.paginator %} + {% include 'pagination.html' with list=cost_estimate_list%} + {% endif %} + + + + + + + + + + + + + + + + + {% for estimate in cost_estimate_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 "Validity" as tr_validity %} + {% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %} + + {% trans "Cost estimate ID" as tr_estimate_id %} + {% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %} + + {% trans "Invoice created" as tr_invoice_created%} + {% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %} +
{{ estimate.recipient }}{{ estimate.name }}{{ estimate.prix_total }}{{ estimate.payment }}{{ estimate.date }}{{ estimate.validity }}{{ estimate.id }} + {% if estimate.final_invoice %} + + {% else %} + ' + {% endif %} + + {% can_edit estimate %} + {% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %} + {% acl_end %} + {% history_button estimate %} + {% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %} + + + + + {% trans "PDF" %} + +
+ + {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} +
diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index a00084f6..c7a6975c 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} + {% if title %} +

{{title}}

+ {% else %}

{% trans "Edit the invoice" %}

+ {% endif %} {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }}

{% trans "Articles" %}

diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 11e490d7..2cfd4f46 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -75,8 +75,12 @@ {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ {% if fid is not None %} + {% if is_estimate %} + {\bf Devis n\textsuperscript{o} :} {{ fid }} & \\ + {% else %} {\bf Facture n\textsuperscript{o} :} {{ fid }} & \\ {% endif %} + {% endif %} \end{tabular*} \\ @@ -104,9 +108,11 @@ \begin{tabular}{|l|r|} \hline \textbf{Total} & {{total|floatformat:2}} \euro \\ + {% if not is_estimate %} \textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\ \doublehline \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ + {% endif %} \hline \end{tabular} @@ -119,6 +125,10 @@ \textbf{Remarque} & {{remark|safe}} \\ \hline {% endif %} + {% if end_validity %} + \textbf{Validité} & Jusqu'au {{end_validity}} \\ + \hline + {% endif %} \end{tabularx} diff --git a/cotisations/templates/cotisations/index_cost_estimate.html b/cotisations/templates/cotisations/index_cost_estimate.html new file mode 100644 index 00000000..a0b3a661 --- /dev/null +++ b/cotisations/templates/cotisations/index_cost_estimate.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 "Cost estimates" %}{% endblock %} + +{% block content %} +

{% trans "Cost estimates list" %}

+{% can_create CostEstimate %} +{% include "buttons/add.html" with href='cotisations:new-cost-estimate'%} +{% acl_end %} +{% include 'cotisations/aff_cost_estimate.html' %} +{% endblock %} diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 4f077fad..c3240a9a 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Custom invoices" %} {% acl_end %} + {% can_view_all CostEstimate %} + + {% trans "Cost estimate" %} + + {% acl_end %} {% can_view_all Article %} {% trans "Available articles" %} diff --git a/cotisations/tex.py b/cotisations/tex.py index d6c0ae5f..4d3715af 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -49,8 +49,9 @@ def render_invoice(_request, ctx={}): Render an invoice using some available information such as the current date, the user, the articles, the prices, ... """ + is_estimate = ctx.get('is_estimate', False) filename = '_'.join([ - 'invoice', + 'cost_estimate' if is_estimate else 'invoice', slugify(ctx.get('asso_name', "")), slugify(ctx.get('recipient_name', "")), str(ctx.get('DATE', datetime.now()).year), diff --git a/cotisations/urls.py b/cotisations/urls.py index edc448fe..45032fe2 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -51,11 +51,41 @@ urlpatterns = [ views.facture_pdf, name='facture-pdf' ), + url( + r'^new_cost_estimate/$', + views.new_cost_estimate, + name='new-cost-estimate' + ), + url( + r'^index_cost_estimate/$', + views.index_cost_estimate, + name='index-cost-estimate' + ), + url( + r'^cost_estimate_pdf/(?P[0-9]+)$', + views.cost_estimate_pdf, + name='cost-estimate-pdf', + ), url( r'^index_custom_invoice/$', views.index_custom_invoice, name='index-custom-invoice' ), + url( + r'^edit_cost_estimate/(?P[0-9]+)$', + views.edit_cost_estimate, + name='edit-cost-estimate' + ), + url( + r'^cost_estimate_to_invoice/(?P[0-9]+)$', + views.cost_estimate_to_invoice, + name='cost-estimate-to-invoice' + ), + url( + r'^del_cost_estimate/(?P[0-9]+)$', + views.del_cost_estimate, + name='del-cost-estimate' + ), url( r'^new_custom_invoice/$', views.new_custom_invoice, diff --git a/cotisations/views.py b/cotisations/views.py index ec746bb7..d4805dc2 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -68,7 +68,8 @@ from .models import ( Paiement, Banque, CustomInvoice, - BaseInvoice + BaseInvoice, + CostEstimate ) from .forms import ( FactureForm, @@ -81,7 +82,8 @@ from .forms import ( SelectArticleForm, RechargeForm, CustomInvoiceForm, - DiscountForm + DiscountForm, + CostEstimateForm, ) from .tex import render_invoice, escape_chars from .payment_methods.forms import payment_method_factory @@ -179,7 +181,58 @@ def new_facture(request, user, userid): ) -# TODO : change facture to invoice +@login_required +@can_create(CostEstimate) +def new_cost_estimate(request): + """ + 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. + """ + # The template needs the list of articles (for the JS part) + articles = Article.objects.filter( + Q(type_user='All') | Q(type_user=request.user.class_name) + ) + # Building the invocie form and the article formset + cost_estimate_form = CostEstimateForm(request.POST or None) + + articles_formset = formset_factory(SelectArticleForm)( + request.POST or None, + form_kwargs={'user': request.user} + ) + discount_form = DiscountForm(request.POST or None) + + if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid(): + cost_estimate_instance = cost_estimate_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=cost_estimate_instance, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + discount_form.apply_to_invoice(cost_estimate_instance) + messages.success( + request, + _("The cost estimate was created.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + + return form({ + 'factureform': cost_estimate_form, + 'action_name': _("Confirm"), + 'articlesformset': articles_formset, + 'articlelist': articles, + 'discount_form': discount_form, + 'title': _("Cost estimate"), + }, 'cotisations/facture.html', request) + + @login_required @can_create(CustomInvoice) def new_custom_invoice(request): @@ -336,6 +389,55 @@ def del_facture(request, facture, **_kwargs): }, 'cotisations/delete.html', request) +@login_required +@can_edit(CostEstimate) +def edit_cost_estimate(request, invoice, **kwargs): + # Building the invocie form and the article formset + invoice_form = CostEstimateForm( + 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 cost estimate was edited.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + + return form({ + 'factureform': invoice_form, + 'venteform': purchase_form, + 'title': "Edit the cost estimate" + }, 'cotisations/edit_facture.html', request) + + +@login_required +@can_edit(CostEstimate) +@can_create(CustomInvoice) +def cost_estimate_to_invoice(request, cost_estimate, **_kwargs): + """Create a custom invoice from a cos estimate""" + cost_estimate.create_invoice() + messages.success( + request, + _("An invoice was successfully created from your cost estimate.") + ) + return redirect(reverse('cotisations:index-custom-invoice')) + + @login_required @can_edit(CustomInvoice) def edit_custom_invoice(request, invoice, **kwargs): @@ -371,6 +473,68 @@ def edit_custom_invoice(request, invoice, **kwargs): }, 'cotisations/edit_facture.html', request) +@login_required +@can_view(CostEstimate) +def cost_estimate_pdf(request, invoice, **_kwargs): + """ + View used to generate a PDF file from an existing cost estimate 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': escape_chars(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), + 'payment_method': invoice.payment, + 'remark': invoice.remark, + 'end_validity': invoice.date + invoice.validity, + 'is_estimate': True, + }) + + +@login_required +@can_delete(CostEstimate) +def del_cost_estimate(request, estimate, **_kwargs): + """ + View used to delete an existing invocie. + """ + if request.method == "POST": + estimate.delete() + messages.success( + request, + _("The cost estimate was deleted.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + return form({ + 'objet': estimate, + 'objet_name': _("Cost Estimate") + }, 'cotisations/delete.html', request) + + @login_required @can_view(CustomInvoice) def custom_invoice_pdf(request, invoice, **_kwargs): @@ -412,7 +576,6 @@ def custom_invoice_pdf(request, invoice, **_kwargs): }) -# TODO : change facture to invoice @login_required @can_delete(CustomInvoice) def del_custom_invoice(request, invoice, **_kwargs): @@ -763,12 +926,35 @@ def index_banque(request): }) +@login_required +@can_view_all(CustomInvoice) +def index_cost_estimate(request): + """View used to display every custom invoice.""" + pagination_number = GeneralOption.get_cached_value('pagination_number') + cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set') + cost_estimate_list = SortTable.sort( + cost_estimate_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_CUSTOM + ) + cost_estimate_list = re2o_paginator( + request, + cost_estimate_list, + pagination_number, + ) + return render(request, 'cotisations/index_cost_estimate.html', { + 'cost_estimate_list': cost_estimate_list + }) + + @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') + cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')] + custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids) custom_invoice_list = SortTable.sort( custom_invoice_list, request.GET.get('col'),