From 6fdf8a0406c15b7aa08a764d1e027a7c77ae0238 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 Jan 2019 19:52:06 +0100 Subject: [PATCH 01/16] Add Document templates to re2o --- cotisations/admin.py | 7 ++ cotisations/forms.py | 35 ++++++ .../migrations/0039_documenttemplate.py | 50 +++++++++ .../cotisations/aff_document_template.html | 50 +++++++++ .../cotisations/index_document_template.html | 42 ++++++++ .../templates/cotisations/sidebar.html | 6 ++ cotisations/tex.py | 26 ++++- cotisations/urls.py | 20 ++++ cotisations/views.py | 102 +++++++++++++++++- install_re2o.sh | 47 +++++--- 10 files changed, 366 insertions(+), 19 deletions(-) create mode 100644 cotisations/migrations/0039_documenttemplate.py create mode 100644 cotisations/templates/cotisations/aff_document_template.html create mode 100644 cotisations/templates/cotisations/index_document_template.html diff --git a/cotisations/admin.py b/cotisations/admin.py index 4b47ccc8..93ad1ab5 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -31,6 +31,7 @@ from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente from .models import CustomInvoice, CostEstimate +from .tex import DocumentTemplate class FactureAdmin(VersionAdmin): @@ -74,6 +75,11 @@ class CotisationAdmin(VersionAdmin): pass +class DocumentTemplateAdmin(VersionAdmin): + """Admin class for DocumentTemplate""" + pass + + admin.site.register(Facture, FactureAdmin) admin.site.register(Article, ArticleAdmin) admin.site.register(Banque, BanqueAdmin) @@ -82,3 +88,4 @@ admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin) admin.site.register(CostEstimate, CostEstimateAdmin) +admin.site.register(DocumentTemplate, DocumentTemplateAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 2eae5287..d512ea80 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -50,6 +50,7 @@ from .models import ( Article, Paiement, Facture, Banque, CustomInvoice, Vente, CostEstimate ) +from .tex import DocumentTemplate from .payment_methods import balance @@ -316,3 +317,37 @@ class RechargeForm(FormRevMixin, Form): } ) return self.cleaned_data + + +class DocumentTemplateForm(FormRevMixin, ModelForm): + """ + Form used to create a document template. + """ + class Meta: + model = DocumentTemplate + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(DocumentTemplateForm, self).__init__( + *args, prefix=prefix, **kwargs) + + +class DelDocumentTemplateForm(FormRevMixin, Form): + """ + Form used to delete one or more document templatess. + The use must choose the one to delete by checking the boxes. + """ + document_templates = forms.ModelMultipleChoiceField( + queryset=DocumentTemplate.objects.none(), + label=_("Available document templates"), + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelDocumentTemplateForm, self).__init__(*args, **kwargs) + if instances: + self.fields['document_templates'].queryset = instances + else: + self.fields['document_templates'].queryset = Banque.objects.all() diff --git a/cotisations/migrations/0039_documenttemplate.py b/cotisations/migrations/0039_documenttemplate.py new file mode 100644 index 00000000..f07506e0 --- /dev/null +++ b/cotisations/migrations/0039_documenttemplate.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-03 16:48 +from __future__ import unicode_literals +import os + +from django.db import migrations, models +from django.core.files import File +from django.conf import settings +import re2o.mixins + + +def create_default_templates(apps, schema_editor): + DocumentTemplate = apps.get_model('cotisations', 'DocumentTemplate') + invoice_path = os.path.join( + settings.BASE_DIR, + "cotisations", + "templates", + "cotisations", + "factures.tex" + ) + with open(invoice_path) as f: + tpl, _ = DocumentTemplate.objects.get_or_create( + name="Re2o default invoice", + ) + tpl.template.save('default_invoice.tex', File(f)) + tpl.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0038_auto_20181231_1657'), + ] + + operations = [ + migrations.CreateModel( + name='DocumentTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('template', models.FileField(upload_to='templates/', verbose_name='template')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ], + options={ + 'verbose_name_plural': 'document templates', + 'verbose_name': 'document template', + }, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + migrations.RunPython(create_default_templates), + ] diff --git a/cotisations/templates/cotisations/aff_document_template.html b/cotisations/templates/cotisations/aff_document_template.html new file mode 100644 index 00000000..e35406d4 --- /dev/null +++ b/cotisations/templates/cotisations/aff_document_template.html @@ -0,0 +1,50 @@ +{% 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 %} +{% load logs_extra %} + + + + + + + + + + {% for template in document_template_list %} + + + + + + {% endfor %} +
{% trans "Document template" %}{% trans "File" %}
{{ template.name }}{{template.template}} + {% can_edit template %} + {% include 'buttons/edit.html' with href='cotisations:edit-document-template' id=template.id %} + {% acl_end %} + {% history_button template %} +
+ diff --git a/cotisations/templates/cotisations/index_document_template.html b/cotisations/templates/cotisations/index_document_template.html new file mode 100644 index 00000000..f5fb9ae2 --- /dev/null +++ b/cotisations/templates/cotisations/index_document_template.html @@ -0,0 +1,42 @@ +{% 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 © 2019 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 bootstrap3 %} +{% load acl %} +{% load i18n %} + +{% block title %}{% trans "Document Templates" %}{% endblock %} + +{% block content %} +

{% trans "Document templates list" %}

+ {% can_create Banque %} + + {% trans "Add a document template" %} + + {% acl_end %} + + {% trans "Delete one or several document templates" %} + + {% include 'cotisations/aff_document_template.html' %} +{% endblock %} + diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 608f95c2..4949cd94 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -65,5 +65,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Payment methods" %} {% acl_end %} + {% can_view_all DocumentTemplate %} + + {% trans "Document templates" %} + + {% acl_end %} + {% endblock %} diff --git a/cotisations/tex.py b/cotisations/tex.py index 4d3715af..873c9346 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -31,12 +31,15 @@ from subprocess import Popen, PIPE import os from datetime import datetime +from django.db import models from django.template.loader import get_template from django.template import Context from django.http import HttpResponse from django.conf import settings from django.utils.text import slugify -import logging +from django.utils.translation import ugettext_lazy as _ + +from re2o.mixins import AclMixin, RevMixin TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') @@ -44,6 +47,27 @@ CACHE_PREFIX = getattr(settings, 'TEX_CACHE_PREFIX', 'render-tex') CACHE_TIMEOUT = getattr(settings, 'TEX_CACHE_TIMEOUT', 86400) # 1 day +class DocumentTemplate(RevMixin, AclMixin, models.Model): + """Represent a template in order to create documents such as invoice or + subscribtion voucher. + """ + template = models.FileField( + upload_to='templates/', + verbose_name=_('template') + ) + name = models.CharField( + max_length=255, + verbose_name=_('name') + ) + + class Meta: + verbose_name = _("document template") + verbose_name_plural = _("document templates") + + def __str__(self): + return str(self.name) + + def render_invoice(_request, ctx={}): """ Render an invoice using some available information such as the current diff --git a/cotisations/urls.py b/cotisations/urls.py index 45032fe2..380052e8 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -176,5 +176,25 @@ urlpatterns = [ views.control, name='control' ), + url( + r'^add_document_template/$', + views.add_document_template, + name='add-document-template' + ), + url( + r'^edit_document_template/(?P[0-9]+)$', + views.edit_document_template, + name='edit-document-template' + ), + url( + r'^del_document_template/$', + views.del_document_template, + name='del-document-template' + ), + url( + r'^index_document_template/$', + views.index_document_template, + name='index-document-template' + ), url(r'^$', views.index, name='index'), ] + payment_methods.urls.urlpatterns diff --git a/cotisations/views.py b/cotisations/views.py index b713f3b9..08139dfc 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -84,8 +84,10 @@ from .forms import ( CustomInvoiceForm, DiscountForm, CostEstimateForm, + DocumentTemplateForm, + DelDocumentTemplateForm ) -from .tex import render_invoice, escape_chars +from .tex import render_invoice, escape_chars, DocumentTemplate from .payment_methods.forms import payment_method_factory from .utils import find_payment_method @@ -720,7 +722,7 @@ def edit_paiement(request, paiement_instance, **_kwargs): if payment_method is not None: payment_method.save() messages.success( - request,_("The payment method was edited.") + request, _("The payment method was edited.") ) return redirect(reverse('cotisations:index-paiement')) return form({ @@ -954,7 +956,8 @@ def index_custom_invoice(request): """View used to display every custom invoice.""" pagination_number = GeneralOption.get_cached_value('pagination_number') 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 = CustomInvoice.objects.prefetch_related( + 'vente_set').exclude(id__in=cost_estimate_ids) custom_invoice_list = SortTable.sort( custom_invoice_list, request.GET.get('col'), @@ -1020,7 +1023,8 @@ def credit_solde(request, user, **_kwargs): kwargs={'userid': user.id} )) - refill_form = RechargeForm(request.POST or None, user=user, user_source=request.user) + refill_form = RechargeForm( + request.POST or None, user=user, user_source=request.user) if refill_form.is_valid(): price = refill_form.cleaned_data['value'] invoice = Facture(user=user) @@ -1050,3 +1054,93 @@ def credit_solde(request, user, **_kwargs): 'max_balance': find_payment_method(p).maximum_balance, }, 'cotisations/facture.html', request) + +@login_required +@can_create(DocumentTemplate) +def add_document_template(request): + """ + View used to add a document template. + """ + document_template = DocumentTemplateForm(request.POST or None) + if document_template.is_valid(): + document_template.save() + messages.success( + request, + _("The document template was created.") + ) + return redirect(reverse('cotisations:index-document-template')) + return form({ + 'factureform': document_template, + 'action_name': _("Add"), + 'title': _("New document template") + }, 'cotisations/facture.html', request) + + +@login_required +@can_edit(DocumentTemplate) +def edit_document_template(request, document_template_instance, **_kwargs): + """ + View used to edit a document_template. + """ + document_template = DocumentTemplateForm( + request.POST or None, instance=document_template_instance) + if document_template.is_valid(): + if document_template.changed_data: + document_template.save() + messages.success( + request, + _("The document template was edited.") + ) + return redirect(reverse('cotisations:index-document-template')) + return form({ + 'factureform': document_template, + 'action_name': _("Edit"), + 'title': _("Edit document template") + }, 'cotisations/facture.html', request) + + +@login_required +@can_delete_set(DocumentTemplate) +def del_document_template(request, instances): + """ + View used to delete a set of document template. + """ + document_template = DelDocumentTemplateForm( + request.POST or None, instances=instances) + if document_template.is_valid(): + document_template_del = document_template.cleaned_data['document_templates'] + for document_template in document_template_del: + try: + document_template.delete() + messages.success( + request, + _("The document template %(document_template)s was deleted.") % { + 'document_template': document_template + } + ) + except ProtectedError: + messages.error( + request, + _("The document template %(document_template)s can't be deleted \ + because it is currently being used.") % { + 'document_template': document_template + } + ) + return redirect(reverse('cotisations:index-document-template')) + return form({ + 'factureform': document_template, + 'action_name': _("Delete"), + 'title': _("Delete document template") + }, 'cotisations/facture.html', request) + + +@login_required +@can_view_all(DocumentTemplate) +def index_document_template(request): + """ + View used to display the list of all available document templates. + """ + document_template_list = DocumentTemplate.objects.order_by('name') + return render(request, 'cotisations/index_document_template.html', { + 'document_template_list': document_template_list + }) diff --git a/install_re2o.sh b/install_re2o.sh index b6d8b2aa..3c32cf4d 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -59,7 +59,7 @@ _ask_value() { install_requirements() { - ### Usage: install_requirements + ### Usage: install_requirements # # This function will install the required packages from APT repository # and Pypi repository. Those packages are all required for Re2o to work @@ -273,7 +273,7 @@ write_settings_file() { django_secret_key="$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(50)]))")" aes_key="$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(32)]))")" - + if [ "$db_engine_type" == 1 ]; then sed -i 's/db_engine/django.db.backends.mysql/g' "$SETTINGS_LOCAL_FILE" else @@ -324,6 +324,19 @@ update_django() { +copy_templates_files() { + ### Usage: copy_templates_files + # + # This will copy LaTeX templates in the media root. + + echo "Copying LaTeX templates ..." + mkdir -p media/templates/ + cp cotisations/templates/cotisations/factures.tex media/templates + echo "Copying LaTeX templates: Done" +} + + + create_superuser() { ### Usage: create_superuser # @@ -476,7 +489,7 @@ interactive_guide() { sql_host="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Prompt to enter the remote database name TITLE="SQL database name" INPUTBOX="The name of the remote SQL database" @@ -523,14 +536,14 @@ interactive_guide() { ldap_is_local="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --menu "$MENU" \ $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" - + # Prompt to enter the LDAP domain extension TITLE="Domain extension" INPUTBOX="The local domain extension to use (e.g. 'example.net'). This is used in the LDAP configuration." extension_locale="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Building the DN of the LDAP from the extension IFS='.' read -a extension_locale_array <<< $extension_locale for i in "${extension_locale_array[@]}" @@ -546,7 +559,7 @@ interactive_guide() { ldap_host="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Prompt to choose if TLS should be activated or not for the LDAP TITLE="TLS on LDAP" MENU="Would you like to activate TLS for communicating with the remote LDAP ?" @@ -583,7 +596,7 @@ interactive_guide() { ######################### BACKTITLE="Re2o setup - configuration of the mail server" - + # Prompt to enter the hostname of the mail server TITLE="Mail server hostname" INPUTBOX="The hostname of the mail server to use" @@ -591,7 +604,7 @@ interactive_guide() { --title "$TITLE" --inputbox "$TITLE" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - # Prompt to choose the port of the mail server + # Prompt to choose the port of the mail server TITLE="Mail server port" MENU="Which port (thus which protocol) to use to contact the mail server" OPTIONS=(25 "SMTP" @@ -608,7 +621,7 @@ interactive_guide() { ######################## BACKTITLE="Re2o setup - configuration of the web server" - + # Prompt to choose the web server TITLE="Web server to use" MENU="Which web server to install for accessing Re2o web frontend (automatic setup of nginx is not supported) ?" @@ -617,14 +630,14 @@ interactive_guide() { web_serveur="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --menu "$MENU" \ $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" - + # Prompt to enter the requested URL for the web frontend TITLE="Web URL" INPUTBOX="URL for accessing the web server (e.g. re2o.example.net). Be sure that this URL is accessible and correspond to a DNS entry (if applicable)." url_server="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Prompt to choose if the TLS should be setup or not for the web server TITLE="TLS on web server" MENU="Would you like to activate the TLS (with Let'Encrypt) on the web server ?" @@ -679,7 +692,7 @@ interactive_guide() { update_django create_superuser - + install_webserver "$web_serveur" "$is_tls" "$url_server" @@ -748,9 +761,10 @@ main_function() { echo " * {help} ---------- Display this quick usage documentation" echo " * {setup} --------- Launch the full interactive guide to setup entirely" echo " re2o from scratch" - echo " * {update} -------- Collect frontend statics, install the missing APT" + echo " * {update} -------- Collect frontend statics, install the missing APT and copy LaTeX templates files" echo " and pip packages and apply the migrations to the DB" echo " * {update-django} - Apply Django migration and collect frontend statics" + echo " * {copy-template-files} - Copy LaTeX templates files to media/templates" echo " * {update-packages} Install the missing APT and pip packages" echo " * {update-settings} Interactively rewrite the settings file" echo " * {reset-db} ------ Erase the previous local database, setup a new empty" @@ -783,6 +797,11 @@ main_function() { update ) install_requirements update_django + copy_templates_files + ;; + + copy-templates-files ) + copy_templates_files ;; update-django ) @@ -800,7 +819,7 @@ main_function() { reset-db ) if [ ! -z "$2" ]; then db_password="$2" - case "$3" in + case "$3" in mysql ) db_engine_type=1;; postresql ) From 0a8335c375554d9118ed412e850efc59cdf3861d Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 Jan 2019 22:34:45 +0100 Subject: [PATCH 02/16] Enable template selection for invoices. --- cotisations/tex.py | 5 ++++- preferences/forms.py | 8 ++++++++ preferences/models.py | 12 ++++++++++++ .../preferences/display_preferences.html | 19 +++++++++++++++++++ preferences/urls.py | 5 +++++ preferences/views.py | 3 +++ re2o/settings.py | 1 + 7 files changed, 52 insertions(+), 1 deletion(-) diff --git a/cotisations/tex.py b/cotisations/tex.py index 873c9346..b253d12b 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -40,6 +40,7 @@ from django.utils.text import slugify from django.utils.translation import ugettext_lazy as _ from re2o.mixins import AclMixin, RevMixin +from preferences.models import CotisationsOption TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') @@ -73,6 +74,7 @@ def render_invoice(_request, ctx={}): Render an invoice using some available information such as the current date, the user, the articles, the prices, ... """ + options, _ = CotisationsOption.objects.get_or_create() is_estimate = ctx.get('is_estimate', False) filename = '_'.join([ 'cost_estimate' if is_estimate else 'invoice', @@ -82,7 +84,8 @@ def render_invoice(_request, ctx={}): str(ctx.get('DATE', datetime.now()).month), str(ctx.get('DATE', datetime.now()).day), ]) - r = render_tex(_request, 'cotisations/factures.tex', ctx) + templatename = options.invoice_template.template.name.split('/')[-1] + r = render_tex(_request, templatename, ctx) r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( name=filename ) diff --git a/preferences/forms.py b/preferences/forms.py index 3d461ef2..d8e762e1 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -43,6 +43,7 @@ from .models import ( RadiusKey, SwitchManagementCred, RadiusOption, + CotisationsOption ) from topologie.models import Switch @@ -253,6 +254,13 @@ class EditRadiusOptionForm(ModelForm): return cleaned_data +class EditCotisationsOptionForm(ModelForm): + """Edition forms for Cotisations options""" + class Meta: + model = CotisationsOption + fields = '__all__' + + class ServiceForm(ModelForm): """Edition, ajout de services sur la page d'accueil""" class Meta: diff --git a/preferences/models.py b/preferences/models.py index daeb4a05..781107af 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -687,3 +687,15 @@ class RadiusOption(AclMixin, PreferencesModel): null=True ) + +class CotisationsOption(AclMixin, PreferencesModel): + class Meta: + verbose_name = _("cotisations options") + + invoice_template = models.OneToOneField( + 'cotisations.DocumentTemplate', + verbose_name=_("Template for invoices"), + related_name="invoice_template", + on_delete=models.PROTECT, + ) + diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 50b2b647..b0fd8dd8 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -346,6 +346,25 @@ with this program; if not, write to the Free Software Foundation, Inc., +
+ +
+ + + + + + + + +
{% trans "Invoices' template" %}{{ cotisationsoptions.invoice_template }}
+
+
+
diff --git a/preferences/urls.py b/preferences/urls.py index 30163868..75ce0a8a 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -71,6 +71,11 @@ urlpatterns = [ views.edit_options, name='edit-options' ), + url( + r'^edit_options/(?P
CotisationsOption)$', + views.edit_options, + name='edit-options' + ), url(r'^add_service/$', views.add_service, name='add-service'), url( r'^edit_service/(?P[0-9]+)$', diff --git a/preferences/views.py b/preferences/views.py index 0e86713c..229b64e8 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -64,6 +64,7 @@ from .models import ( RadiusKey, SwitchManagementCred, RadiusOption, + CotisationsOption, ) from . import models from . import forms @@ -88,6 +89,7 @@ def display_options(request): radiuskey_list = RadiusKey.objects.all() switchmanagementcred_list = SwitchManagementCred.objects.all() radiusoptions, _ = RadiusOption.objects.get_or_create() + cotisationsoptions, _created = CotisationsOption.objects.get_or_create() return form({ 'useroptions': useroptions, 'machineoptions': machineoptions, @@ -102,6 +104,7 @@ def display_options(request): 'radiuskey_list' : radiuskey_list, 'switchmanagementcred_list': switchmanagementcred_list, 'radiusoptions' : radiusoptions, + 'cotisationsoptions': cotisationsoptions, }, 'preferences/display_preferences.html', request) diff --git a/re2o/settings.py b/re2o/settings.py index 0a019d2d..f2557b44 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -120,6 +120,7 @@ TEMPLATES = [ 'DIRS': [ # Use only absolute paths with '/' delimiters even on Windows os.path.join(BASE_DIR, 'templates').replace('\\', '/'), + os.path.join(BASE_DIR, 'media', 'templates').replace('\\', '/'), ], 'APP_DIRS': True, 'OPTIONS': { From 48d8d7921db99f8365c9c8790b7431852ea2bc5c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sat, 5 Jan 2019 19:45:21 +0100 Subject: [PATCH 03/16] subscripbtion voucher --- cotisations/admin.py | 3 +- cotisations/forms.py | 3 +- cotisations/models.py | 52 ++++++++++++++++++ cotisations/tex.py | 42 +++++++-------- cotisations/utils.py | 53 +++++++++++++++++++ cotisations/views.py | 5 +- .../migrations/0057_cotisationsoption.py | 38 +++++++++++++ preferences/models.py | 7 ++- users/models.py | 21 +++++++- .../users/email_subscription_accepted | 22 ++++++++ 10 files changed, 217 insertions(+), 29 deletions(-) create mode 100644 preferences/migrations/0057_cotisationsoption.py create mode 100644 users/templates/users/email_subscription_accepted diff --git a/cotisations/admin.py b/cotisations/admin.py index 93ad1ab5..9eb0f071 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,8 +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, CostEstimate -from .tex import DocumentTemplate +from .models import CustomInvoice, CostEstimate, DocumentTemplate class FactureAdmin(VersionAdmin): diff --git a/cotisations/forms.py b/cotisations/forms.py index d512ea80..21f8f42f 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -48,9 +48,8 @@ from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin from .models import ( Article, Paiement, Facture, Banque, - CustomInvoice, Vente, CostEstimate + CustomInvoice, Vente, CostEstimate, DocumentTemplate ) -from .tex import DocumentTemplate from .payment_methods import balance diff --git a/cotisations/models.py b/cotisations/models.py index ea565ac8..7ca4d4a9 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -236,11 +236,23 @@ class Facture(BaseInvoice): 'control': self.can_change_control, } self.__original_valid = self.valid + self.__original_control = self.control + + def get_subscribtion(self): + return self.vent_set.filter( + Q(type_cotisation='All') | + Q(type_cotisation='Cotisation') + ) + + def is_subscribtion(self): + return bool(self.get_subscribtion()) def save(self, *args, **kwargs): super(Facture, self).save(*args, **kwargs) if not self.__original_valid and self.valid: send_mail_invoice(self) + if self.is_subscribtion() and not self.__original_control and self.control: + send_mail_voucher(self) def __str__(self): return str(self.user) + ' ' + str(self.date) @@ -255,6 +267,10 @@ def facture_post_save(**kwargs): user = facture.user user.set_active() user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + if facture.control: + user = facture.user + if user.is_adherent(): + user.notif_subscription_accepted() @receiver(post_delete, sender=Facture) @@ -935,3 +951,39 @@ def cotisation_post_delete(**_kwargs): """ regen('mac_ip_list') regen('mailing') + + +class DocumentTemplate(RevMixin, AclMixin, models.Model): + """Represent a template in order to create documents such as invoice or + subscribtion voucher. + """ + template = models.FileField( + upload_to='templates/', + verbose_name=_('template') + ) + name = models.CharField( + max_length=255, + verbose_name=_('name') + ) + + class Meta: + verbose_name = _("document template") + verbose_name_plural = _("document templates") + + def __str__(self): + return str(self.name) + + +class Voucher(RevMixin, AclMixin, models.Model): + """A Subscription Voucher.""" + user = models.ForeignKey( + 'users.User', + on_delete=models.CASCADE, + verbose_name=_("user") + ) + + class Meta: + verbose_name = _("subscription voucher") + + def __str__(self): + return "voucher {} {}".format(self.user, self.date) diff --git a/cotisations/tex.py b/cotisations/tex.py index b253d12b..0d6dc31f 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -48,27 +48,6 @@ CACHE_PREFIX = getattr(settings, 'TEX_CACHE_PREFIX', 'render-tex') CACHE_TIMEOUT = getattr(settings, 'TEX_CACHE_TIMEOUT', 86400) # 1 day -class DocumentTemplate(RevMixin, AclMixin, models.Model): - """Represent a template in order to create documents such as invoice or - subscribtion voucher. - """ - template = models.FileField( - upload_to='templates/', - verbose_name=_('template') - ) - name = models.CharField( - max_length=255, - verbose_name=_('name') - ) - - class Meta: - verbose_name = _("document template") - verbose_name_plural = _("document templates") - - def __str__(self): - return str(self.name) - - def render_invoice(_request, ctx={}): """ Render an invoice using some available information such as the current @@ -92,6 +71,27 @@ def render_invoice(_request, ctx={}): return r +def render_voucher(_request, ctx={}): + """ + Render a subscribtion voucher. + """ + options, _ = CotisationsOption.objects.get_or_create() + filename = '_'.join([ + 'voucher', + slugify(ctx.get('asso_name', "")), + slugify(ctx.get('recipient_name', "")), + str(ctx.get('DATE', datetime.now()).year), + str(ctx.get('DATE', datetime.now()).month), + str(ctx.get('DATE', datetime.now()).day), + ]) + templatename = options.voucher_template.template.name.split('/')[-1] + r = create_pdf(templatename, ctx) + r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( + name=filename + ) + return r + + def create_pdf(template, ctx={}): """Creates and returns a PDF from a LaTeX template using pdflatex. diff --git a/cotisations/utils.py b/cotisations/utils.py index a8c2768e..a7856540 100644 --- a/cotisations/utils.py +++ b/cotisations/utils.py @@ -93,3 +93,56 @@ def send_mail_invoice(invoice): attachments=[('invoice.pdf', pdf, 'application/pdf')] ) mail.send() + + +def send_mail_voucher(invoice): + """Creates a voucher from an invoice and sends it by email to the client""" + purchases_info = [] + for purchase in invoice.vente_set.all(): + purchases_info.append({ + 'name': purchase.name, + 'price': purchase.prix, + 'quantity': purchase.number, + 'total_price': purchase.prix_total + }) + + ctx = { + 'paid': True, + 'fid': invoice.id, + 'DATE': invoice.date, + 'recipient_name': "{} {}".format( + invoice.user.name, + invoice.user.surname + ), + 'address': invoice.user.room, + '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) + } + + pdf = create_pdf('cotisations/factures.tex', ctx) + template = get_template('cotisations/email_invoice') + + ctx = { + 'name': "{} {}".format( + invoice.user.name, + invoice.user.surname + ), + 'contact_mail': AssoOption.get_cached_value('contact'), + 'asso_name': AssoOption.get_cached_value('name') + } + + mail = EmailMessage( + 'Votre facture / Your invoice', + template.render(ctx), + GeneralOption.get_cached_value('email_from'), + [invoice.user.get_mail], + attachments=[('invoice.pdf', pdf, 'application/pdf')] + ) + mail.send() diff --git a/cotisations/views.py b/cotisations/views.py index 08139dfc..38609305 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -69,7 +69,8 @@ from .models import ( Banque, CustomInvoice, BaseInvoice, - CostEstimate + CostEstimate, + DocumentTemplate ) from .forms import ( FactureForm, @@ -87,7 +88,7 @@ from .forms import ( DocumentTemplateForm, DelDocumentTemplateForm ) -from .tex import render_invoice, escape_chars, DocumentTemplate +from .tex import render_invoice, escape_chars from .payment_methods.forms import payment_method_factory from .utils import find_payment_method diff --git a/preferences/migrations/0057_cotisationsoption.py b/preferences/migrations/0057_cotisationsoption.py new file mode 100644 index 00000000..d02d9c28 --- /dev/null +++ b/preferences/migrations/0057_cotisationsoption.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-03 19:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +def initialize_invoice_template(apps, schema_editor): + CotisationsOption = apps.get_model('preferences', 'CotisationsOption') + DocumentTemplate = apps.get_model('cotisations', 'DocumentTemplate') + CotisationsOption.objects.create( + invoice_template=DocumentTemplate.objects.first() + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0039_documenttemplate'), + ('preferences', '0056_4_radiusoption'), + ] + + operations = [ + migrations.CreateModel( + name='CotisationsOption', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('invoice_template', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='cotisations.DocumentTemplate', verbose_name='Template for invoices')), + ], + options={ + 'verbose_name': 'cotisations options', + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.RunPython(initialize_invoice_template), + ] diff --git a/preferences/models.py b/preferences/models.py index 781107af..99750850 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -698,4 +698,9 @@ class CotisationsOption(AclMixin, PreferencesModel): related_name="invoice_template", on_delete=models.PROTECT, ) - + voucher_template = models.OneToOneField( + 'cotisations.DocumentTemplate', + verbose_name=_("Template for subscription voucher"), + related_name="voucher_template", + on_delete=models.PROTECT, + ) diff --git a/users/models.py b/users/models.py index c1d0789a..7d91b2b5 100755 --- a/users/models.py +++ b/users/models.py @@ -663,7 +663,26 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ) return - def reset_passwd_mail(self, request): + def notif_subscription_accepted(self): + """Send an email when the subscription has been accepted""" + template = loader.get_template('users/email_subscription_accepted') + mailmessageoptions, _created = MailMessageOption\ + .objects.get_or_create() + context = Context({ + 'nom': self.get_full_name(), + 'asso_name': AssoOption.get_cached_value('name'), + 'asso_email': AssoOption.get_cached_value('contact'), + }) + send_mail( + 'Votre inscription a été validée / Your subscription has been accepted', + '', + GeneralOption.get_cached_value('email_from'), + [self.email], + html_message=template.render(context) + ) + return + + def reset_passwd_mail(self, request): """ Prend en argument un request, envoie un mail de réinitialisation de mot de pass """ req = Request() diff --git a/users/templates/users/email_subscription_accepted b/users/templates/users/email_subscription_accepted new file mode 100644 index 00000000..2ce5c839 --- /dev/null +++ b/users/templates/users/email_subscription_accepted @@ -0,0 +1,22 @@ +

Bonjour {{nom}} !

+ +

Nous vous informons que votre cotisation auprès de {{asso_name}} a été acceptée. Vous voilà donc membre de l'association.

+ +

Vous trouverez en pièce jointe un reçu.

+ +

Pour nous faire part de toute remarque, suggestion ou problème vous pouvez nous envoyer un mail à {{asso_email}}.

+ +

À bientôt,
+L'équipe de {{asso_name}}.

+ +

---

+ +

Your subscription to {{asso_name}} has just been accepted. You are now a full member of {{asso_name}}. + +

You will find with this email a subscription voucher.

+ +

For any information, suggestion or problem, you can contact us via email at
+{{asso_email}}.

+ +

Regards,
+The {{asso_name}} team.

From 713b4b5c665d21824dbc4829a7176fc96ab75b43 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 10 Jan 2019 23:15:36 +0100 Subject: [PATCH 04/16] Fix queryset for HostMacIpView --- api/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/views.py b/api/views.py index 8f7b9c1f..3108f9f3 100644 --- a/api/views.py +++ b/api/views.py @@ -611,9 +611,11 @@ class HostMacIpView(generics.ListAPIView): """Exposes the associations between hostname, mac address and IPv4 in order to build the DHCP lease files. """ - queryset = all_active_interfaces() serializer_class = serializers.HostMacIpSerializer + def get_queryset(self): + return all_active_interfaces() + # Firewall @@ -646,7 +648,7 @@ class DNSZonesView(generics.ListAPIView): class DNSReverseZonesView(generics.ListAPIView): - """Exposes the detailed information about each extension (hostnames, + """Exposes the detailed information about each extension (hostnames, IPs, DNS records, etc.) in order to build the DNS zone files. """ queryset = (machines.IpType.objects.all()) From b83bfc0da9300700ff6866645d451987dc9ddc23 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Fri, 11 Jan 2019 00:39:16 +0100 Subject: [PATCH 05/16] Fix #123 Subscription voucher --- .../migrations/0039_documenttemplate.py | 21 ----- cotisations/models.py | 41 +++------ .../cotisations/aff_cost_estimate.html | 2 +- .../cotisations/aff_cotisations.html | 19 ++-- .../cotisations/email_subscription_accepted | 22 +++++ cotisations/templates/cotisations/voucher.tex | 87 +++++++++++++++++++ cotisations/tex.py | 24 ++--- cotisations/urls.py | 5 ++ cotisations/utils.py | 46 +++------- cotisations/views.py | 31 ++++++- install_re2o.sh | 3 +- preferences/forms.py | 3 - .../migrations/0057_cotisationsoption.py | 33 ++++++- .../migrations/0058_assooption_pres_name.py | 20 +++++ preferences/models.py | 6 ++ .../preferences/display_preferences.html | 8 ++ users/models.py | 21 +---- .../users/email_subscription_accepted | 22 ----- 18 files changed, 265 insertions(+), 149 deletions(-) create mode 100644 cotisations/templates/cotisations/email_subscription_accepted create mode 100644 cotisations/templates/cotisations/voucher.tex create mode 100644 preferences/migrations/0058_assooption_pres_name.py delete mode 100644 users/templates/users/email_subscription_accepted diff --git a/cotisations/migrations/0039_documenttemplate.py b/cotisations/migrations/0039_documenttemplate.py index f07506e0..ee93da92 100644 --- a/cotisations/migrations/0039_documenttemplate.py +++ b/cotisations/migrations/0039_documenttemplate.py @@ -1,31 +1,11 @@ # -*- coding: utf-8 -*- # Generated by Django 1.10.7 on 2019-01-03 16:48 from __future__ import unicode_literals -import os from django.db import migrations, models -from django.core.files import File -from django.conf import settings import re2o.mixins -def create_default_templates(apps, schema_editor): - DocumentTemplate = apps.get_model('cotisations', 'DocumentTemplate') - invoice_path = os.path.join( - settings.BASE_DIR, - "cotisations", - "templates", - "cotisations", - "factures.tex" - ) - with open(invoice_path) as f: - tpl, _ = DocumentTemplate.objects.get_or_create( - name="Re2o default invoice", - ) - tpl.template.save('default_invoice.tex', File(f)) - tpl.save() - - class Migration(migrations.Migration): dependencies = [ @@ -46,5 +26,4 @@ class Migration(migrations.Migration): }, bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), ), - migrations.RunPython(create_default_templates), ] diff --git a/cotisations/models.py b/cotisations/models.py index 7ca4d4a9..42d76a08 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -50,7 +50,9 @@ from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin -from cotisations.utils import find_payment_method, send_mail_invoice +from cotisations.utils import ( + find_payment_method, send_mail_invoice, send_mail_voucher +) from cotisations.validators import check_no_balance @@ -238,20 +240,22 @@ class Facture(BaseInvoice): self.__original_valid = self.valid self.__original_control = self.control - def get_subscribtion(self): - return self.vent_set.filter( - Q(type_cotisation='All') | - Q(type_cotisation='Cotisation') + def get_subscription(self): + return Cotisation.objects.filter( + vente__in=self.vente_set.filter( + Q(type_cotisation='All') | + Q(type_cotisation='Cotisation') + ) ) - def is_subscribtion(self): - return bool(self.get_subscribtion()) + def is_subscription(self): + return bool(self.get_subscription()) def save(self, *args, **kwargs): super(Facture, self).save(*args, **kwargs) if not self.__original_valid and self.valid: send_mail_invoice(self) - if self.is_subscribtion() and not self.__original_control and self.control: + if self.is_subscription() and not self.__original_control and self.control: send_mail_voucher(self) def __str__(self): @@ -267,10 +271,6 @@ def facture_post_save(**kwargs): user = facture.user user.set_active() user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) - if facture.control: - user = facture.user - if user.is_adherent(): - user.notif_subscription_accepted() @receiver(post_delete, sender=Facture) @@ -955,7 +955,7 @@ def cotisation_post_delete(**_kwargs): class DocumentTemplate(RevMixin, AclMixin, models.Model): """Represent a template in order to create documents such as invoice or - subscribtion voucher. + subscription voucher. """ template = models.FileField( upload_to='templates/', @@ -972,18 +972,3 @@ class DocumentTemplate(RevMixin, AclMixin, models.Model): def __str__(self): return str(self.name) - - -class Voucher(RevMixin, AclMixin, models.Model): - """A Subscription Voucher.""" - user = models.ForeignKey( - 'users.User', - on_delete=models.CASCADE, - verbose_name=_("user") - ) - - class Meta: - verbose_name = _("subscription voucher") - - def __str__(self): - return "voucher {} {}".format(self.user, self.date) diff --git a/cotisations/templates/cotisations/aff_cost_estimate.html b/cotisations/templates/cotisations/aff_cost_estimate.html index e591a5fe..eb040dce 100644 --- a/cotisations/templates/cotisations/aff_cost_estimate.html +++ b/cotisations/templates/cotisations/aff_cost_estimate.html @@ -75,7 +75,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if estimate.final_invoice %} {% else %} - ' + {% endif %} diff --git a/cotisations/templates/cotisations/aff_cotisations.html b/cotisations/templates/cotisations/aff_cotisations.html index 7dd64395..e27ae8c7 100644 --- a/cotisations/templates/cotisations/aff_cotisations.html +++ b/cotisations/templates/cotisations/aff_cotisations.html @@ -48,7 +48,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Date" as tr_date %} {% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %} - + {% trans "Invoice ID" as tr_invoice_id %} {% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %} @@ -63,17 +63,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ facture.prix_total }} {{ facture.paiement }} {{ facture.date }} - {{ facture.id }} + {{ facture.id }} {% can_edit facture %} - {% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %} + {% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %} {% acl_else %} {% trans "Controlled invoice" %} {% acl_end %} {% can_delete facture %} - {% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %} + {% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %} {% acl_end %} - {% history_button facture %} + {% history_button facture %} {% if facture.valid %} @@ -83,13 +83,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% else %} {% trans "Invalidated invoice" %} {% endif %} + {% if facture.control and facture.is_subscription %} + + {% trans "Voucher" %} + + {% endif %} {% endfor %} -{% if facture_list.paginator %} + {% if facture_list.paginator %} {% include 'pagination.html' with list=facture_list %} -{% endif %} + {% endif %}
diff --git a/cotisations/templates/cotisations/email_subscription_accepted b/cotisations/templates/cotisations/email_subscription_accepted new file mode 100644 index 00000000..58027cec --- /dev/null +++ b/cotisations/templates/cotisations/email_subscription_accepted @@ -0,0 +1,22 @@ +Bonjour {{name}} ! + +Nous vous informons que votre cotisation auprès de {{asso_name}} a été acceptée. Vous voilà donc membre de l'association. + +Vous trouverez en pièce jointe un reçu. + +Pour nous faire part de toute remarque, suggestion ou problème vous pouvez nous envoyer un mail à {{asso_email}}. + +À bientôt, +L'équipe de {{asso_name}}. + +--- + +Your subscription to {{asso_name}} has just been accepted. You are now a full member of {{asso_name}}. + +You will find with this email a subscription voucher. + +For any information, suggestion or problem, you can contact us via email at +{{asso_email}}. + +Regards, +The {{asso_name}} team. diff --git a/cotisations/templates/cotisations/voucher.tex b/cotisations/templates/cotisations/voucher.tex new file mode 100644 index 00000000..aeebc187 --- /dev/null +++ b/cotisations/templates/cotisations/voucher.tex @@ -0,0 +1,87 @@ +{% load i18n %} +{% language 'fr' %} + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Invoice Template +% LaTeX Template +% Version 1.0 (3/11/12) +%% This template has been downloaded from: +% http://www.LaTeXTemplates.com +% +% Original author: +% Trey Hunner (http://www.treyhunner.com/) +% +% License: +% CC BY-NC-SA 3.0 (http://creativecommons.org/licenses/by-nc-sa/3.0/) +% +% Important note: +% This template requires the invoice.cls file to be in the same directory as +% the .tex file. The invoice.cls file provides the style used for structuring the +% document. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%---------------------------------------------------------------------------------------- +% DOCUMENT CONFIGURATION +%---------------------------------------------------------------------------------------- + +\documentclass[12pt]{article} % Use the custom invoice class (invoice.cls) +\usepackage[utf8]{inputenc} +\usepackage[letterpaper,hmargin=0.79in,vmargin=0.79in]{geometry} +\usepackage{longtable} +\usepackage{graphicx} +\usepackage{tabularx} +\usepackage{eurosym} +\usepackage{multicol} + +\pagestyle{empty} % No page numbers + +\linespread{1.5} + +\newcommand{\doublehline}{\noalign{\hrule height 1pt}} +\setlength{\parindent}{0cm} + + +\begin{document} + + %---------------------------------------------------------------------------------------- + % HEADING SECTION + %---------------------------------------------------------------------------------------- + \begin{center} + {\Huge\bf Reçu d'adhésion \\ {{asso_name|safe}} } % Company providing the invoice + \end{center} + + \bigskip + \hrule + \bigskip + + \vfill + + Je sousigné, {{pres_name|safe}}, déclare par la présente avoir reçu le bulletin d'adhésion de: + + \begin{center} + \setlength{\tabcolsep}{10pt} % Make table columns tighter, usefull for postionning + \begin{tabular}{r l r l} + {\bf Prénom :}~ & {{firstname|safe}} & {% if phone %}{\bf Téléphone :}~ & {{phone}}{% else %} & {% endif %} \\ + {\bf Nom :}~ & {{lastname|safe}} & {\bf Mail :}~ & {{email|safe}} \\ + \end{tabular} + \end{center} + \bigskip + + ainsi que sa cotisation. + + Le postulant, déclare reconnaître l'objet de l'association, et en a accepté les statuts ainsi que le règlement intérieur qui sont mis à sa disposition dans les locaux de l'association. L'adhésion du membre sus-nommé est ainsi validée. Ce reçu confirme la qualité de membre du postulant, et ouvre droit à la participation à l'assemblée générale de l'association jusqu'au {{date_end|date:"d F Y"}}. + + \bigskip + + Validé électroniquement par {{pres_name|safe}}, le {{date_begin|date:"d/m/Y"}}. + + \vfill + \hrule + \smallskip + \footnotesize + Les informations recueillies sont nécessaires pour votre adhésion. Conformément à la loi "Informatique et Libertés" du 6 janvier 1978, vous disposez d'un droit d'accès et de rectification aux données personnelles vous concernant. Pour l'exercer, adressez-vous au secrétariat de l'association. + + +\end{document} +{% endlanguage %} diff --git a/cotisations/tex.py b/cotisations/tex.py index 0d6dc31f..1ab964ba 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -79,13 +79,14 @@ def render_voucher(_request, ctx={}): filename = '_'.join([ 'voucher', slugify(ctx.get('asso_name', "")), - slugify(ctx.get('recipient_name', "")), - str(ctx.get('DATE', datetime.now()).year), - str(ctx.get('DATE', datetime.now()).month), - str(ctx.get('DATE', datetime.now()).day), + slugify(ctx.get('firstname', "")), + slugify(ctx.get('lastname', "")), + str(ctx.get('date_begin', datetime.now()).year), + str(ctx.get('date_begin', datetime.now()).month), + str(ctx.get('date_begin', datetime.now()).day), ]) templatename = options.voucher_template.template.name.split('/')[-1] - r = create_pdf(templatename, ctx) + r = render_tex(_request, templatename, ctx) r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( name=filename ) @@ -110,12 +111,13 @@ def create_pdf(template, ctx={}): with tempfile.TemporaryDirectory() as tempdir: for _ in range(2): - process = Popen( - ['pdflatex', '-output-directory', tempdir], - stdin=PIPE, - stdout=PIPE, - ) - process.communicate(rendered_tpl) + with open("/var/www/re2o/out.log", "w") as f: + process = Popen( + ['pdflatex', '-output-directory', tempdir], + stdin=PIPE, + stdout=f,#PIPE, + ) + process.communicate(rendered_tpl) with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f: pdf = f.read() diff --git a/cotisations/urls.py b/cotisations/urls.py index 380052e8..8dbb84ec 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -51,6 +51,11 @@ urlpatterns = [ views.facture_pdf, name='facture-pdf' ), + url( + r'^voucher_pdf/(?P[0-9]+)$', + views.voucher_pdf, + name='voucher-pdf' + ), url( r'^new_cost_estimate/$', views.new_cost_estimate, diff --git a/cotisations/utils.py b/cotisations/utils.py index a7856540..4715338b 100644 --- a/cotisations/utils.py +++ b/cotisations/utils.py @@ -25,7 +25,7 @@ from django.template.loader import get_template from django.core.mail import EmailMessage from .tex import create_pdf -from preferences.models import AssoOption, GeneralOption +from preferences.models import AssoOption, GeneralOption, CotisationsOption from re2o.settings import LOGO_PATH from re2o import settings @@ -97,52 +97,34 @@ def send_mail_invoice(invoice): def send_mail_voucher(invoice): """Creates a voucher from an invoice and sends it by email to the client""" - purchases_info = [] - for purchase in invoice.vente_set.all(): - purchases_info.append({ - 'name': purchase.name, - 'price': purchase.prix, - 'quantity': purchase.number, - 'total_price': purchase.prix_total - }) - ctx = { - 'paid': True, - 'fid': invoice.id, - 'DATE': invoice.date, - 'recipient_name': "{} {}".format( - invoice.user.name, - invoice.user.surname - ), - 'address': invoice.user.room, - '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) + 'pres_name': AssoOption.get_cached_value('pres_name'), + 'firstname': invoice.user.name, + 'lastname': invoice.user.surname, + 'email': invoice.user.email, + 'phone': invoice.user.telephone, + 'date_end': invoice.get_subscription().latest('date_end').date_end, + 'date_begin': invoice.get_subscription().earliest('date_start').date_start } - - pdf = create_pdf('cotisations/factures.tex', ctx) - template = get_template('cotisations/email_invoice') + templatename = CotisationsOption.get_cached_value('voucher_template').template.name.split('/')[-1] + pdf = create_pdf(templatename, ctx) + template = get_template('cotisations/email_subscription_accepted') ctx = { 'name': "{} {}".format( invoice.user.name, invoice.user.surname ), - 'contact_mail': AssoOption.get_cached_value('contact'), + 'asso_email': AssoOption.get_cached_value('contact'), 'asso_name': AssoOption.get_cached_value('name') } mail = EmailMessage( - 'Votre facture / Your invoice', + 'Votre reçu / Your voucher', template.render(ctx), GeneralOption.get_cached_value('email_from'), [invoice.user.get_mail], - attachments=[('invoice.pdf', pdf, 'application/pdf')] + attachments=[('voucher.pdf', pdf, 'application/pdf')] ) mail.send() diff --git a/cotisations/views.py b/cotisations/views.py index 38609305..98157210 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -88,7 +88,7 @@ from .forms import ( DocumentTemplateForm, DelDocumentTemplateForm ) -from .tex import render_invoice, escape_chars +from .tex import render_invoice, render_voucher, escape_chars from .payment_methods.forms import payment_method_factory from .utils import find_payment_method @@ -220,6 +220,7 @@ def new_cost_estimate(request): number=quantity ) discount_form.apply_to_invoice(cost_estimate_instance) + messages.success( request, _("The cost estimate was created.") @@ -485,7 +486,6 @@ def cost_estimate_pdf(request, invoice, **_kwargs): 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) @@ -1145,3 +1145,30 @@ def index_document_template(request): return render(request, 'cotisations/index_document_template.html', { 'document_template_list': document_template_list }) + + +@login_required +@can_view(Facture) +def voucher_pdf(request, invoice, **_kwargs): + """ + View used to generate a PDF file from a controlled invoice + 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. + """ + if not invoice.control: + messages.error( + request, + _("Could not find a voucher for that invoice.") + ) + return redirect(reverse('cotisations:index')) + return render_voucher(request, { + 'asso_name': AssoOption.get_cached_value('name'), + 'pres_name': AssoOption.get_cached_value('pres_name'), + 'firstname': invoice.user.name, + 'lastname': invoice.user.surname, + 'email': invoice.user.email, + 'phone': invoice.user.telephone, + 'date_end': invoice.get_subscription().latest('date_end').date_end, + 'date_begin': invoice.get_subscription().earliest('date_start').date_start + }) diff --git a/install_re2o.sh b/install_re2o.sh index 3c32cf4d..d64cdb50 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -331,7 +331,8 @@ copy_templates_files() { echo "Copying LaTeX templates ..." mkdir -p media/templates/ - cp cotisations/templates/cotisations/factures.tex media/templates + cp cotisations/templates/cotisations/factures.tex media/templates/default_invoice.tex + cp cotisations/templates/cotisations/voucher.tex media/templates/default_voucher.tex echo "Copying LaTeX templates: Done" } diff --git a/preferences/forms.py b/preferences/forms.py index d8e762e1..b1902d8d 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -183,9 +183,6 @@ class EditAssoOptionForm(ModelForm): self.fields['pseudo'].label = _("Usual name") self.fields['utilisateur_asso'].label = _("Account used for editing" " from /admin") - self.fields['payment'].label = _("Payment") - self.fields['payment_id'].label = _("Payment ID") - self.fields['payment_pass'].label = _("Payment password") self.fields['description'].label = _("Description") diff --git a/preferences/migrations/0057_cotisationsoption.py b/preferences/migrations/0057_cotisationsoption.py index d02d9c28..99c8bcc2 100644 --- a/preferences/migrations/0057_cotisationsoption.py +++ b/preferences/migrations/0057_cotisationsoption.py @@ -1,17 +1,47 @@ # -*- coding: utf-8 -*- # Generated by Django 1.10.7 on 2019-01-03 19:56 from __future__ import unicode_literals +import os from django.db import migrations, models import django.db.models.deletion +from django.core.files import File +from django.conf import settings import re2o.mixins def initialize_invoice_template(apps, schema_editor): CotisationsOption = apps.get_model('preferences', 'CotisationsOption') DocumentTemplate = apps.get_model('cotisations', 'DocumentTemplate') + invoice_path = os.path.join( + settings.BASE_DIR, + "cotisations", + "templates", + "cotisations", + "factures.tex" + ) + voucher_path = os.path.join( + settings.BASE_DIR, + "cotisations", + "templates", + "cotisations", + "voucher.tex" + ) + with open(invoice_path) as f: + tpl_invoice, _ = DocumentTemplate.objects.get_or_create( + name="Re2o default invoice", + ) + tpl_invoice.template.save('default_invoice.tex', File(f)) + tpl_invoice.save() + with open(voucher_path) as f: + tpl_voucher, _ = DocumentTemplate.objects.get_or_create( + name="Re2o default voucher", + ) + tpl_voucher.template.save('default_voucher.tex', File(f)) + tpl_voucher.save() CotisationsOption.objects.create( - invoice_template=DocumentTemplate.objects.first() + invoice_template=tpl_invoice, + voucher_template=tpl_voucher, ) @@ -28,6 +58,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('invoice_template', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='cotisations.DocumentTemplate', verbose_name='Template for invoices')), + ('voucher_template', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='cotisations.DocumentTemplate', verbose_name='Template for subscription voucher')), ], options={ 'verbose_name': 'cotisations options', diff --git a/preferences/migrations/0058_assooption_pres_name.py b/preferences/migrations/0058_assooption_pres_name.py new file mode 100644 index 00000000..bcc480af --- /dev/null +++ b/preferences/migrations/0058_assooption_pres_name.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-10 22:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0057_cotisationsoption'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='pres_name', + field=models.CharField(default='', help_text='Displayed on subscription vouchers', max_length=255, verbose_name='President of the association'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 99750850..f03a6719 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -521,6 +521,12 @@ class AssoOption(AclMixin, PreferencesModel): null=True, blank=True, ) + pres_name = models.CharField( + max_length=255, + default="", + verbose_name=_("President of the association"), + help_text=_("Displayed on subscription vouchers") + ) class Meta: permissions = ( diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index b0fd8dd8..92ac0915 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -343,6 +343,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Description of the organisation" %} {{ assooptions.description|safe }} + + {% trans "President of the association"%} + {{ assooptions.pres_name }} +
@@ -361,6 +365,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Invoices' template" %} {{ cotisationsoptions.invoice_template }} + + {% trans "Vouchers' template" %} + {{ cotisationsoptions.voucher_template }} + diff --git a/users/models.py b/users/models.py index 7d91b2b5..c1d0789a 100755 --- a/users/models.py +++ b/users/models.py @@ -663,26 +663,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ) return - def notif_subscription_accepted(self): - """Send an email when the subscription has been accepted""" - template = loader.get_template('users/email_subscription_accepted') - mailmessageoptions, _created = MailMessageOption\ - .objects.get_or_create() - context = Context({ - 'nom': self.get_full_name(), - 'asso_name': AssoOption.get_cached_value('name'), - 'asso_email': AssoOption.get_cached_value('contact'), - }) - send_mail( - 'Votre inscription a été validée / Your subscription has been accepted', - '', - GeneralOption.get_cached_value('email_from'), - [self.email], - html_message=template.render(context) - ) - return - - def reset_passwd_mail(self, request): + def reset_passwd_mail(self, request): """ Prend en argument un request, envoie un mail de réinitialisation de mot de pass """ req = Request() diff --git a/users/templates/users/email_subscription_accepted b/users/templates/users/email_subscription_accepted deleted file mode 100644 index 2ce5c839..00000000 --- a/users/templates/users/email_subscription_accepted +++ /dev/null @@ -1,22 +0,0 @@ -

Bonjour {{nom}} !

- -

Nous vous informons que votre cotisation auprès de {{asso_name}} a été acceptée. Vous voilà donc membre de l'association.

- -

Vous trouverez en pièce jointe un reçu.

- -

Pour nous faire part de toute remarque, suggestion ou problème vous pouvez nous envoyer un mail à {{asso_email}}.

- -

À bientôt,
-L'équipe de {{asso_name}}.

- -

---

- -

Your subscription to {{asso_name}} has just been accepted. You are now a full member of {{asso_name}}. - -

You will find with this email a subscription voucher.

- -

For any information, suggestion or problem, you can contact us via email at
-{{asso_email}}.

- -

Regards,
-The {{asso_name}} team.

From ebdb45507aea39f582326d25117f3d3fd897a0e5 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Fri, 11 Jan 2019 00:47:00 +0100 Subject: [PATCH 06/16] CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdacea61..40ca06d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,3 +164,17 @@ Collec new statics ```bash python3 manage.py collectstatic ``` + +## MR 391: Document templates and subscription vouchers + +Re2o can now use templates for generated invoices. To load default templates run + +```bash +./install update +``` + +Be carefull, you need the proper rights to edit a DocumentTemplate. + +Re2o now sends subscription voucher when an invoice is controlled. It uses one +of the templates. You also need to set the name of the president of your association +to be set in your settings. From 124421e011eca9e68d3df8d9026a00ae31f8e5af Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Fri, 11 Jan 2019 15:30:23 +0100 Subject: [PATCH 07/16] Fix migrations --- .../{0057_cotisationsoption.py => 0058_cotisationsoption.py} | 2 +- ...058_assooption_pres_name.py => 0059_assooption_pres_name.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename preferences/migrations/{0057_cotisationsoption.py => 0058_cotisationsoption.py} (97%) rename preferences/migrations/{0058_assooption_pres_name.py => 0059_assooption_pres_name.py} (90%) diff --git a/preferences/migrations/0057_cotisationsoption.py b/preferences/migrations/0058_cotisationsoption.py similarity index 97% rename from preferences/migrations/0057_cotisationsoption.py rename to preferences/migrations/0058_cotisationsoption.py index 99c8bcc2..2f3ee3ac 100644 --- a/preferences/migrations/0057_cotisationsoption.py +++ b/preferences/migrations/0058_cotisationsoption.py @@ -49,7 +49,7 @@ class Migration(migrations.Migration): dependencies = [ ('cotisations', '0039_documenttemplate'), - ('preferences', '0056_4_radiusoption'), + ('preferences', '0057_optionaluser_all_users_active'), ] operations = [ diff --git a/preferences/migrations/0058_assooption_pres_name.py b/preferences/migrations/0059_assooption_pres_name.py similarity index 90% rename from preferences/migrations/0058_assooption_pres_name.py rename to preferences/migrations/0059_assooption_pres_name.py index bcc480af..a2003c66 100644 --- a/preferences/migrations/0058_assooption_pres_name.py +++ b/preferences/migrations/0059_assooption_pres_name.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('preferences', '0057_cotisationsoption'), + ('preferences', '0058_cotisationsoption'), ] operations = [ From d323ed5937e2a79ea1adf464c59ed4aecb00bcbf Mon Sep 17 00:00:00 2001 From: detraz Date: Fri, 11 Jan 2019 20:58:02 +0100 Subject: [PATCH 08/16] Start date set au moment de la validation de la facture --- cotisations/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cotisations/views.py b/cotisations/views.py index 98157210..ea9e6f44 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -1170,5 +1170,5 @@ def voucher_pdf(request, invoice, **_kwargs): 'email': invoice.user.email, 'phone': invoice.user.telephone, 'date_end': invoice.get_subscription().latest('date_end').date_end, - 'date_begin': invoice.get_subscription().earliest('date_start').date_start + 'date_begin': invoice.date }) From 1fcf90b69812d29beee5a5483f47c514240a9d47 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 20 Jan 2019 17:47:01 +0100 Subject: [PATCH 09/16] fix migrations --- .../{0058_cotisationsoption.py => 0059_cotisationsoption.py} | 2 +- ...059_assooption_pres_name.py => 0060_assooption_pres_name.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename preferences/migrations/{0058_cotisationsoption.py => 0059_cotisationsoption.py} (97%) rename preferences/migrations/{0059_assooption_pres_name.py => 0060_assooption_pres_name.py} (90%) diff --git a/preferences/migrations/0058_cotisationsoption.py b/preferences/migrations/0059_cotisationsoption.py similarity index 97% rename from preferences/migrations/0058_cotisationsoption.py rename to preferences/migrations/0059_cotisationsoption.py index 2f3ee3ac..1d36bf06 100644 --- a/preferences/migrations/0058_cotisationsoption.py +++ b/preferences/migrations/0059_cotisationsoption.py @@ -49,7 +49,7 @@ class Migration(migrations.Migration): dependencies = [ ('cotisations', '0039_documenttemplate'), - ('preferences', '0057_optionaluser_all_users_active'), + ('preferences', '0058_auto_20190108_1650'), ] operations = [ diff --git a/preferences/migrations/0059_assooption_pres_name.py b/preferences/migrations/0060_assooption_pres_name.py similarity index 90% rename from preferences/migrations/0059_assooption_pres_name.py rename to preferences/migrations/0060_assooption_pres_name.py index a2003c66..986ad511 100644 --- a/preferences/migrations/0059_assooption_pres_name.py +++ b/preferences/migrations/0060_assooption_pres_name.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('preferences', '0058_cotisationsoption'), + ('preferences', '0059_cotisationsoption'), ] operations = [ From f43b985966cd1e188e4b4e572cc703312c177859 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 20 Jan 2019 18:53:25 +0100 Subject: [PATCH 10/16] Meilleure gestion des default pour les templates invoice et voucher --- install_re2o.sh | 2 +- .../migrations/0059_cotisationsoption.py | 40 ++----------------- preferences/models.py | 19 +++++++++ 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/install_re2o.sh b/install_re2o.sh index d64cdb50..ff09e6d8 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -797,8 +797,8 @@ main_function() { update ) install_requirements - update_django copy_templates_files + update_django ;; copy-templates-files ) diff --git a/preferences/migrations/0059_cotisationsoption.py b/preferences/migrations/0059_cotisationsoption.py index 1d36bf06..9fe53203 100644 --- a/preferences/migrations/0059_cotisationsoption.py +++ b/preferences/migrations/0059_cotisationsoption.py @@ -1,48 +1,16 @@ # -*- coding: utf-8 -*- # Generated by Django 1.10.7 on 2019-01-03 19:56 from __future__ import unicode_literals -import os from django.db import migrations, models import django.db.models.deletion -from django.core.files import File -from django.conf import settings import re2o.mixins +import preferences.models def initialize_invoice_template(apps, schema_editor): CotisationsOption = apps.get_model('preferences', 'CotisationsOption') - DocumentTemplate = apps.get_model('cotisations', 'DocumentTemplate') - invoice_path = os.path.join( - settings.BASE_DIR, - "cotisations", - "templates", - "cotisations", - "factures.tex" - ) - voucher_path = os.path.join( - settings.BASE_DIR, - "cotisations", - "templates", - "cotisations", - "voucher.tex" - ) - with open(invoice_path) as f: - tpl_invoice, _ = DocumentTemplate.objects.get_or_create( - name="Re2o default invoice", - ) - tpl_invoice.template.save('default_invoice.tex', File(f)) - tpl_invoice.save() - with open(voucher_path) as f: - tpl_voucher, _ = DocumentTemplate.objects.get_or_create( - name="Re2o default voucher", - ) - tpl_voucher.template.save('default_voucher.tex', File(f)) - tpl_voucher.save() - CotisationsOption.objects.create( - invoice_template=tpl_invoice, - voucher_template=tpl_voucher, - ) + CotisationsOption.objects.get_or_create() class Migration(migrations.Migration): @@ -57,8 +25,8 @@ class Migration(migrations.Migration): name='CotisationsOption', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invoice_template', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='cotisations.DocumentTemplate', verbose_name='Template for invoices')), - ('voucher_template', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='cotisations.DocumentTemplate', verbose_name='Template for subscription voucher')), + ('invoice_template', models.OneToOneField(default=preferences.models.default_invoice,on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='cotisations.DocumentTemplate', verbose_name='Template for invoices')), + ('voucher_template', models.OneToOneField(default=preferences.models.default_voucher, on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='cotisations.DocumentTemplate', verbose_name='Template for subscription voucher')), ], options={ 'verbose_name': 'cotisations options', diff --git a/preferences/models.py b/preferences/models.py index f03a6719..71f1ffa0 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -35,6 +35,7 @@ from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ import machines.models +import cotisations.models from re2o.mixins import AclMixin from re2o.aes_field import AESEncryptedField @@ -694,6 +695,22 @@ class RadiusOption(AclMixin, PreferencesModel): ) +def default_invoice(): + tpl, _ = cotisations.models.DocumentTemplate.objects.get_or_create( + name="Re2o default invoice", + template="templates/default_invoice.tex" + ) + return tpl.id + + +def default_voucher(): + tpl, _ = cotisations.models.DocumentTemplate.objects.get_or_create( + name="Re2o default voucher", + template="templates/default_voucher.tex" + ) + return tpl.id + + class CotisationsOption(AclMixin, PreferencesModel): class Meta: verbose_name = _("cotisations options") @@ -703,10 +720,12 @@ class CotisationsOption(AclMixin, PreferencesModel): verbose_name=_("Template for invoices"), related_name="invoice_template", on_delete=models.PROTECT, + default=default_invoice, ) voucher_template = models.OneToOneField( 'cotisations.DocumentTemplate', verbose_name=_("Template for subscription voucher"), related_name="voucher_template", on_delete=models.PROTECT, + default=default_voucher, ) From 985a2f4a52e9300ed419e8158d039e5a38e00486 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 20 Jan 2019 19:08:11 +0100 Subject: [PATCH 11/16] =?UTF-8?q?Choix=20de=20l'envoi=20du=20re=C3=A7u=20p?= =?UTF-8?q?ar=20mail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/models.py | 6 +++++- ...061_cotisationsoption_send_voucher_mail.py | 20 +++++++++++++++++++ preferences/models.py | 4 ++++ .../preferences/display_preferences.html | 4 ++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 preferences/migrations/0061_cotisationsoption_send_voucher_mail.py diff --git a/cotisations/models.py b/cotisations/models.py index 42d76a08..64ef791d 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -46,6 +46,7 @@ from django.urls import reverse from django.shortcuts import redirect from django.contrib import messages +from preferences.models import CotisationsOption from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin @@ -255,7 +256,10 @@ class Facture(BaseInvoice): super(Facture, self).save(*args, **kwargs) if not self.__original_valid and self.valid: send_mail_invoice(self) - if self.is_subscription() and not self.__original_control and self.control: + if self.is_subscription() \ + and not self.__original_control \ + and self.control \ + and CotisationsOption.get_cached_value('send_voucher_mail'): send_mail_voucher(self) def __str__(self): diff --git a/preferences/migrations/0061_cotisationsoption_send_voucher_mail.py b/preferences/migrations/0061_cotisationsoption_send_voucher_mail.py new file mode 100644 index 00000000..1476ef12 --- /dev/null +++ b/preferences/migrations/0061_cotisationsoption_send_voucher_mail.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-20 18:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0060_assooption_pres_name'), + ] + + operations = [ + migrations.AddField( + model_name='cotisationsoption', + name='send_voucher_mail', + field=models.BooleanField(default=False, verbose_name='Send voucher by email when the invoice is controlled.'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 71f1ffa0..9e2988e0 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -729,3 +729,7 @@ class CotisationsOption(AclMixin, PreferencesModel): on_delete=models.PROTECT, default=default_voucher, ) + send_voucher_mail = models.BooleanField( + verbose_name=_("Send voucher by email when the invoice is controlled."), + default=False, + ) diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 92ac0915..f41e8397 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -361,6 +361,10 @@ with this program; if not, write to the Free Software Foundation, Inc., + + + From ddc2c6e3808cf9bcdf08587f34bb5e55f35ea26a Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Sun, 20 Jan 2019 19:32:29 +0100 Subject: [PATCH 12/16] =?UTF-8?q?Suppression=20des=20anciens=20templates?= =?UTF-8?q?=20lors=20de=20la=20mise=20=C3=A0=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/models.py | 32 +++++++++++++++++++ .../templates/cotisations/facture.html | 2 +- cotisations/views.py | 9 ++++-- install_re2o.sh | 1 + 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index 64ef791d..d7178f20 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -33,6 +33,7 @@ each. from __future__ import unicode_literals from dateutil.relativedelta import relativedelta +import os from django.db import models from django.db.models import Q, Max @@ -976,3 +977,34 @@ class DocumentTemplate(RevMixin, AclMixin, models.Model): def __str__(self): return str(self.name) + +@receiver(models.signals.post_delete, sender=DocumentTemplate) +def auto_delete_file_on_delete(sender, instance, **kwargs): + """ + Deletes file from filesystem + when corresponding `DocumentTemplate` object is deleted. + """ + if instance.template: + if os.path.isfile(instance.template.path): + os.remove(instance.template.path) + + +@receiver(models.signals.pre_save, sender=DocumentTemplate) +def auto_delete_file_on_change(sender, instance, **kwargs): + """ + Deletes old file from filesystem + when corresponding `DocumentTemplate` object is updated + with new file. + """ + if not instance.pk: + return False + + try: + old_file = DocumentTemplate.objects.get(pk=instance.pk).template + except DocumentTemplate.DoesNotExist: + return False + + new_file = instance.template + if not old_file == new_file: + if os.path.isfile(old_file.path): + os.remove(old_file.path) diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 65b05199..5dddb305 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% bootstrap_form_errors discount_form %} {% endif %} -
+ {% csrf_token %} {% bootstrap_form factureform %} {% if payment_method %} diff --git a/cotisations/views.py b/cotisations/views.py index ea9e6f44..e4b25453 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -1062,7 +1062,10 @@ def add_document_template(request): """ View used to add a document template. """ - document_template = DocumentTemplateForm(request.POST or None) + document_template = DocumentTemplateForm( + request.POST or None, + request.FILES or None, + ) if document_template.is_valid(): document_template.save() messages.success( @@ -1084,7 +1087,9 @@ def edit_document_template(request, document_template_instance, **_kwargs): View used to edit a document_template. """ document_template = DocumentTemplateForm( - request.POST or None, instance=document_template_instance) + request.POST or None, + request.FILES or None, + instance=document_template_instance) if document_template.is_valid(): if document_template.changed_data: document_template.save() diff --git a/install_re2o.sh b/install_re2o.sh index ff09e6d8..d6b0a4ef 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -333,6 +333,7 @@ copy_templates_files() { mkdir -p media/templates/ cp cotisations/templates/cotisations/factures.tex media/templates/default_invoice.tex cp cotisations/templates/cotisations/voucher.tex media/templates/default_voucher.tex + chown -R www-data:www-data media/templates/ echo "Copying LaTeX templates: Done" } From f4b6f10d1e3437edc3e07c79e7045c62cc6ae8e5 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 21 Jan 2019 00:08:53 +0100 Subject: [PATCH 13/16] attribut name unique --- .../migrations/0040_auto_20190120_1708.py | 20 +++++++++++++++++++ cotisations/models.py | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 cotisations/migrations/0040_auto_20190120_1708.py diff --git a/cotisations/migrations/0040_auto_20190120_1708.py b/cotisations/migrations/0040_auto_20190120_1708.py new file mode 100644 index 00000000..b7f5b279 --- /dev/null +++ b/cotisations/migrations/0040_auto_20190120_1708.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-20 23:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0039_documenttemplate'), + ] + + operations = [ + migrations.AlterField( + model_name='documenttemplate', + name='name', + field=models.CharField(max_length=125, unique=True, verbose_name='name'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index d7178f20..88407d33 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -967,8 +967,9 @@ class DocumentTemplate(RevMixin, AclMixin, models.Model): verbose_name=_('template') ) name = models.CharField( - max_length=255, - verbose_name=_('name') + max_length=125, + verbose_name=_('name'), + unique=True ) class Meta: From 4fef4a6057a72d34188262e6ba4db4bddd77fa46 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 21 Jan 2019 00:54:02 +0100 Subject: [PATCH 14/16] =?UTF-8?q?D=C3=A9place=20les=20templates=20dans=20p?= =?UTF-8?q?r=C3=A9f=C3=A9rences.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cotisations/admin.py | 8 +- cotisations/forms.py | 36 +------ .../migrations/0039_documenttemplate.py | 29 ------ .../migrations/0040_auto_20190120_1708.py | 20 ---- cotisations/models.py | 54 ---------- .../templates/cotisations/facture.html | 2 +- .../cotisations/index_document_template.html | 42 -------- cotisations/urls.py | 22 +---- cotisations/views.py | 99 ------------------- preferences/admin.py | 10 +- preferences/forms.py | 37 ++++++- .../migrations/0059_auto_20190120_1739.py | 62 ++++++++++++ .../migrations/0059_cotisationsoption.py | 37 ------- .../migrations/0060_assooption_pres_name.py | 20 ---- ...061_cotisationsoption_send_voucher_mail.py | 20 ---- preferences/models.py | 65 ++++++++++-- .../preferences}/aff_document_template.html | 2 +- .../preferences/display_preferences.html | 19 ++++ preferences/urls.py | 15 +++ preferences/views.py | 90 ++++++++++++++++- 20 files changed, 294 insertions(+), 395 deletions(-) delete mode 100644 cotisations/migrations/0039_documenttemplate.py delete mode 100644 cotisations/migrations/0040_auto_20190120_1708.py delete mode 100644 cotisations/templates/cotisations/index_document_template.html create mode 100644 preferences/migrations/0059_auto_20190120_1739.py delete mode 100644 preferences/migrations/0059_cotisationsoption.py delete mode 100644 preferences/migrations/0060_assooption_pres_name.py delete mode 100644 preferences/migrations/0061_cotisationsoption_send_voucher_mail.py rename {cotisations/templates/cotisations => preferences/templates/preferences}/aff_document_template.html (96%) diff --git a/cotisations/admin.py b/cotisations/admin.py index 9eb0f071..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, CostEstimate, DocumentTemplate +from .models import CustomInvoice, CostEstimate class FactureAdmin(VersionAdmin): @@ -74,11 +74,6 @@ class CotisationAdmin(VersionAdmin): pass -class DocumentTemplateAdmin(VersionAdmin): - """Admin class for DocumentTemplate""" - pass - - admin.site.register(Facture, FactureAdmin) admin.site.register(Article, ArticleAdmin) admin.site.register(Banque, BanqueAdmin) @@ -87,4 +82,3 @@ admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin) admin.site.register(CostEstimate, CostEstimateAdmin) -admin.site.register(DocumentTemplate, DocumentTemplateAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 21f8f42f..3f99382b 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -48,7 +48,7 @@ from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin from .models import ( Article, Paiement, Facture, Banque, - CustomInvoice, Vente, CostEstimate, DocumentTemplate + CustomInvoice, Vente, CostEstimate, ) from .payment_methods import balance @@ -316,37 +316,3 @@ class RechargeForm(FormRevMixin, Form): } ) return self.cleaned_data - - -class DocumentTemplateForm(FormRevMixin, ModelForm): - """ - Form used to create a document template. - """ - class Meta: - model = DocumentTemplate - fields = '__all__' - - def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(DocumentTemplateForm, self).__init__( - *args, prefix=prefix, **kwargs) - - -class DelDocumentTemplateForm(FormRevMixin, Form): - """ - Form used to delete one or more document templatess. - The use must choose the one to delete by checking the boxes. - """ - document_templates = forms.ModelMultipleChoiceField( - queryset=DocumentTemplate.objects.none(), - label=_("Available document templates"), - widget=forms.CheckboxSelectMultiple - ) - - def __init__(self, *args, **kwargs): - instances = kwargs.pop('instances', None) - super(DelDocumentTemplateForm, self).__init__(*args, **kwargs) - if instances: - self.fields['document_templates'].queryset = instances - else: - self.fields['document_templates'].queryset = Banque.objects.all() diff --git a/cotisations/migrations/0039_documenttemplate.py b/cotisations/migrations/0039_documenttemplate.py deleted file mode 100644 index ee93da92..00000000 --- a/cotisations/migrations/0039_documenttemplate.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2019-01-03 16:48 -from __future__ import unicode_literals - -from django.db import migrations, models -import re2o.mixins - - -class Migration(migrations.Migration): - - dependencies = [ - ('cotisations', '0038_auto_20181231_1657'), - ] - - operations = [ - migrations.CreateModel( - name='DocumentTemplate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('template', models.FileField(upload_to='templates/', verbose_name='template')), - ('name', models.CharField(max_length=255, verbose_name='name')), - ], - options={ - 'verbose_name_plural': 'document templates', - 'verbose_name': 'document template', - }, - bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), - ), - ] diff --git a/cotisations/migrations/0040_auto_20190120_1708.py b/cotisations/migrations/0040_auto_20190120_1708.py deleted file mode 100644 index b7f5b279..00000000 --- a/cotisations/migrations/0040_auto_20190120_1708.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2019-01-20 23:08 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('cotisations', '0039_documenttemplate'), - ] - - operations = [ - migrations.AlterField( - model_name='documenttemplate', - name='name', - field=models.CharField(max_length=125, unique=True, verbose_name='name'), - ), - ] diff --git a/cotisations/models.py b/cotisations/models.py index 88407d33..7346a634 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -33,7 +33,6 @@ each. from __future__ import unicode_literals from dateutil.relativedelta import relativedelta -import os from django.db import models from django.db.models import Q, Max @@ -956,56 +955,3 @@ def cotisation_post_delete(**_kwargs): """ regen('mac_ip_list') regen('mailing') - - -class DocumentTemplate(RevMixin, AclMixin, models.Model): - """Represent a template in order to create documents such as invoice or - subscription voucher. - """ - template = models.FileField( - upload_to='templates/', - verbose_name=_('template') - ) - name = models.CharField( - max_length=125, - verbose_name=_('name'), - unique=True - ) - - class Meta: - verbose_name = _("document template") - verbose_name_plural = _("document templates") - - def __str__(self): - return str(self.name) - -@receiver(models.signals.post_delete, sender=DocumentTemplate) -def auto_delete_file_on_delete(sender, instance, **kwargs): - """ - Deletes file from filesystem - when corresponding `DocumentTemplate` object is deleted. - """ - if instance.template: - if os.path.isfile(instance.template.path): - os.remove(instance.template.path) - - -@receiver(models.signals.pre_save, sender=DocumentTemplate) -def auto_delete_file_on_change(sender, instance, **kwargs): - """ - Deletes old file from filesystem - when corresponding `DocumentTemplate` object is updated - with new file. - """ - if not instance.pk: - return False - - try: - old_file = DocumentTemplate.objects.get(pk=instance.pk).template - except DocumentTemplate.DoesNotExist: - return False - - new_file = instance.template - if not old_file == new_file: - if os.path.isfile(old_file.path): - os.remove(old_file.path) diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 5dddb305..65b05199 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% bootstrap_form_errors discount_form %} {% endif %} - + {% csrf_token %} {% bootstrap_form factureform %} {% if payment_method %} diff --git a/cotisations/templates/cotisations/index_document_template.html b/cotisations/templates/cotisations/index_document_template.html deleted file mode 100644 index f5fb9ae2..00000000 --- a/cotisations/templates/cotisations/index_document_template.html +++ /dev/null @@ -1,42 +0,0 @@ -{% 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 © 2019 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 bootstrap3 %} -{% load acl %} -{% load i18n %} - -{% block title %}{% trans "Document Templates" %}{% endblock %} - -{% block content %} -

{% trans "Document templates list" %}

- {% can_create Banque %} - - {% trans "Add a document template" %} - - {% acl_end %} - - {% trans "Delete one or several document templates" %} - - {% include 'cotisations/aff_document_template.html' %} -{% endblock %} - diff --git a/cotisations/urls.py b/cotisations/urls.py index 8dbb84ec..0c7256f7 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -181,25 +181,5 @@ urlpatterns = [ views.control, name='control' ), - url( - r'^add_document_template/$', - views.add_document_template, - name='add-document-template' - ), - url( - r'^edit_document_template/(?P[0-9]+)$', - views.edit_document_template, - name='edit-document-template' - ), - url( - r'^del_document_template/$', - views.del_document_template, - name='del-document-template' - ), - url( - r'^index_document_template/$', - views.index_document_template, - name='index-document-template' - ), - url(r'^$', views.index, name='index'), + url(r'^$', views.index, name='index'), ] + payment_methods.urls.urlpatterns diff --git a/cotisations/views.py b/cotisations/views.py index e4b25453..b6d5a8a6 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -70,7 +70,6 @@ from .models import ( CustomInvoice, BaseInvoice, CostEstimate, - DocumentTemplate ) from .forms import ( FactureForm, @@ -85,8 +84,6 @@ from .forms import ( CustomInvoiceForm, DiscountForm, CostEstimateForm, - DocumentTemplateForm, - DelDocumentTemplateForm ) from .tex import render_invoice, render_voucher, escape_chars from .payment_methods.forms import payment_method_factory @@ -1056,102 +1053,6 @@ def credit_solde(request, user, **_kwargs): }, 'cotisations/facture.html', request) -@login_required -@can_create(DocumentTemplate) -def add_document_template(request): - """ - View used to add a document template. - """ - document_template = DocumentTemplateForm( - request.POST or None, - request.FILES or None, - ) - if document_template.is_valid(): - document_template.save() - messages.success( - request, - _("The document template was created.") - ) - return redirect(reverse('cotisations:index-document-template')) - return form({ - 'factureform': document_template, - 'action_name': _("Add"), - 'title': _("New document template") - }, 'cotisations/facture.html', request) - - -@login_required -@can_edit(DocumentTemplate) -def edit_document_template(request, document_template_instance, **_kwargs): - """ - View used to edit a document_template. - """ - document_template = DocumentTemplateForm( - request.POST or None, - request.FILES or None, - instance=document_template_instance) - if document_template.is_valid(): - if document_template.changed_data: - document_template.save() - messages.success( - request, - _("The document template was edited.") - ) - return redirect(reverse('cotisations:index-document-template')) - return form({ - 'factureform': document_template, - 'action_name': _("Edit"), - 'title': _("Edit document template") - }, 'cotisations/facture.html', request) - - -@login_required -@can_delete_set(DocumentTemplate) -def del_document_template(request, instances): - """ - View used to delete a set of document template. - """ - document_template = DelDocumentTemplateForm( - request.POST or None, instances=instances) - if document_template.is_valid(): - document_template_del = document_template.cleaned_data['document_templates'] - for document_template in document_template_del: - try: - document_template.delete() - messages.success( - request, - _("The document template %(document_template)s was deleted.") % { - 'document_template': document_template - } - ) - except ProtectedError: - messages.error( - request, - _("The document template %(document_template)s can't be deleted \ - because it is currently being used.") % { - 'document_template': document_template - } - ) - return redirect(reverse('cotisations:index-document-template')) - return form({ - 'factureform': document_template, - 'action_name': _("Delete"), - 'title': _("Delete document template") - }, 'cotisations/facture.html', request) - - -@login_required -@can_view_all(DocumentTemplate) -def index_document_template(request): - """ - View used to display the list of all available document templates. - """ - document_template_list = DocumentTemplate.objects.order_by('name') - return render(request, 'cotisations/index_document_template.html', { - 'document_template_list': document_template_list - }) - - @login_required @can_view(Facture) def voucher_pdf(request, invoice, **_kwargs): diff --git a/preferences/admin.py b/preferences/admin.py index b7b171d0..efeefc87 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -40,7 +40,8 @@ from .models import ( HomeOption, RadiusKey, SwitchManagementCred, - Reminder + Reminder, + DocumentTemplate ) @@ -101,6 +102,12 @@ class ReminderAdmin(VersionAdmin): """Class reminder for switch""" pass + +class DocumentTemplateAdmin(VersionAdmin): + """Admin class for DocumentTemplate""" + pass + + admin.site.register(OptionalUser, OptionalUserAdmin) admin.site.register(OptionalMachine, OptionalMachineAdmin) admin.site.register(OptionalTopologie, OptionalTopologieAdmin) @@ -113,3 +120,4 @@ admin.site.register(RadiusKey, RadiusKeyAdmin) admin.site.register(SwitchManagementCred, SwitchManagementCredAdmin) admin.site.register(AssoOption, AssoOptionAdmin) admin.site.register(MailMessageOption, MailMessageOptionAdmin) +admin.site.register(DocumentTemplate, DocumentTemplateAdmin) diff --git a/preferences/forms.py b/preferences/forms.py index b1902d8d..d2bede7c 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -43,10 +43,12 @@ from .models import ( RadiusKey, SwitchManagementCred, RadiusOption, - CotisationsOption + CotisationsOption, + DocumentTemplate ) from topologie.models import Switch + class EditOptionalUserForm(ModelForm): """Formulaire d'édition des options de l'user. (solde, telephone..)""" class Meta: @@ -376,3 +378,36 @@ class DelMailContactForm(Form): else: self.fields['mailcontacts'].queryset = MailContact.objects.all() + +class DocumentTemplateForm(FormRevMixin, ModelForm): + """ + Form used to create a document template. + """ + class Meta: + model = DocumentTemplate + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(DocumentTemplateForm, self).__init__( + *args, prefix=prefix, **kwargs) + + +class DelDocumentTemplateForm(FormRevMixin, Form): + """ + Form used to delete one or more document templatess. + The use must choose the one to delete by checking the boxes. + """ + document_templates = forms.ModelMultipleChoiceField( + queryset=DocumentTemplate.objects.none(), + label=_("Available document templates"), + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelDocumentTemplateForm, self).__init__(*args, **kwargs) + if instances: + self.fields['document_templates'].queryset = instances + else: + self.fields['document_templates'].queryset = Banque.objects.all() diff --git a/preferences/migrations/0059_auto_20190120_1739.py b/preferences/migrations/0059_auto_20190120_1739.py new file mode 100644 index 00000000..23447ce8 --- /dev/null +++ b/preferences/migrations/0059_auto_20190120_1739.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-20 23:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import preferences.models +import re2o.mixins + + +def create_defaults(apps, schema_editor): + CotisationsOption = apps.get_model('preferences', 'CotisationsOption') + CotisationsOption.objects.get_or_create() + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0058_auto_20190108_1650'), + ] + + operations = [ + migrations.CreateModel( + name='CotisationsOption', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('send_voucher_mail', models.BooleanField(default=False, verbose_name='Send voucher by email when the invoice is controlled.')), + ], + options={ + 'verbose_name': 'cotisations options', + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='DocumentTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('template', models.FileField(upload_to='templates/', verbose_name='template')), + ('name', models.CharField(max_length=125, unique=True, verbose_name='name')), + ], + options={ + 'verbose_name': 'document template', + 'verbose_name_plural': 'document templates', + }, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='assooption', + name='pres_name', + field=models.CharField(default='', help_text='Displayed on subscription vouchers', max_length=255, verbose_name='President of the association'), + ), + migrations.AddField( + model_name='cotisationsoption', + name='invoice_template', + field=models.OneToOneField(default=preferences.models.default_invoice, on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='preferences.DocumentTemplate', verbose_name='Template for invoices'), + ), + migrations.AddField( + model_name='cotisationsoption', + name='voucher_template', + field=models.OneToOneField(default=preferences.models.default_voucher, on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='preferences.DocumentTemplate', verbose_name='Template for subscription voucher'), + ), + migrations.RunPython(create_defaults), + ] diff --git a/preferences/migrations/0059_cotisationsoption.py b/preferences/migrations/0059_cotisationsoption.py deleted file mode 100644 index 9fe53203..00000000 --- a/preferences/migrations/0059_cotisationsoption.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2019-01-03 19:56 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion -import re2o.mixins -import preferences.models - - -def initialize_invoice_template(apps, schema_editor): - CotisationsOption = apps.get_model('preferences', 'CotisationsOption') - CotisationsOption.objects.get_or_create() - - -class Migration(migrations.Migration): - - dependencies = [ - ('cotisations', '0039_documenttemplate'), - ('preferences', '0058_auto_20190108_1650'), - ] - - operations = [ - migrations.CreateModel( - name='CotisationsOption', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invoice_template', models.OneToOneField(default=preferences.models.default_invoice,on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='cotisations.DocumentTemplate', verbose_name='Template for invoices')), - ('voucher_template', models.OneToOneField(default=preferences.models.default_voucher, on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='cotisations.DocumentTemplate', verbose_name='Template for subscription voucher')), - ], - options={ - 'verbose_name': 'cotisations options', - }, - bases=(re2o.mixins.AclMixin, models.Model), - ), - migrations.RunPython(initialize_invoice_template), - ] diff --git a/preferences/migrations/0060_assooption_pres_name.py b/preferences/migrations/0060_assooption_pres_name.py deleted file mode 100644 index 986ad511..00000000 --- a/preferences/migrations/0060_assooption_pres_name.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2019-01-10 22:13 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('preferences', '0059_cotisationsoption'), - ] - - operations = [ - migrations.AddField( - model_name='assooption', - name='pres_name', - field=models.CharField(default='', help_text='Displayed on subscription vouchers', max_length=255, verbose_name='President of the association'), - ), - ] diff --git a/preferences/migrations/0061_cotisationsoption_send_voucher_mail.py b/preferences/migrations/0061_cotisationsoption_send_voucher_mail.py deleted file mode 100644 index 1476ef12..00000000 --- a/preferences/migrations/0061_cotisationsoption_send_voucher_mail.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.7 on 2019-01-20 18:03 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('preferences', '0060_assooption_pres_name'), - ] - - operations = [ - migrations.AddField( - model_name='cotisationsoption', - name='send_voucher_mail', - field=models.BooleanField(default=False, verbose_name='Send voucher by email when the invoice is controlled.'), - ), - ] diff --git a/preferences/models.py b/preferences/models.py index 9e2988e0..445d7139 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -24,6 +24,7 @@ Reglages généraux, machines, utilisateurs, mail, general pour l'application. """ from __future__ import unicode_literals +import os from django.utils.functional import cached_property from django.utils import timezone @@ -35,9 +36,8 @@ from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ import machines.models -import cotisations.models -from re2o.mixins import AclMixin +from re2o.mixins import AclMixin, RevMixin from re2o.aes_field import AESEncryptedField from datetime import timedelta @@ -696,7 +696,7 @@ class RadiusOption(AclMixin, PreferencesModel): def default_invoice(): - tpl, _ = cotisations.models.DocumentTemplate.objects.get_or_create( + tpl, _ = DocumentTemplate.objects.get_or_create( name="Re2o default invoice", template="templates/default_invoice.tex" ) @@ -704,7 +704,7 @@ def default_invoice(): def default_voucher(): - tpl, _ = cotisations.models.DocumentTemplate.objects.get_or_create( + tpl, _ = DocumentTemplate.objects.get_or_create( name="Re2o default voucher", template="templates/default_voucher.tex" ) @@ -716,14 +716,14 @@ class CotisationsOption(AclMixin, PreferencesModel): verbose_name = _("cotisations options") invoice_template = models.OneToOneField( - 'cotisations.DocumentTemplate', + 'preferences.DocumentTemplate', verbose_name=_("Template for invoices"), related_name="invoice_template", on_delete=models.PROTECT, default=default_invoice, ) voucher_template = models.OneToOneField( - 'cotisations.DocumentTemplate', + 'preferences.DocumentTemplate', verbose_name=_("Template for subscription voucher"), related_name="voucher_template", on_delete=models.PROTECT, @@ -733,3 +733,56 @@ class CotisationsOption(AclMixin, PreferencesModel): verbose_name=_("Send voucher by email when the invoice is controlled."), default=False, ) + + +class DocumentTemplate(RevMixin, AclMixin, models.Model): + """Represent a template in order to create documents such as invoice or + subscription voucher. + """ + template = models.FileField( + upload_to='templates/', + verbose_name=_('template') + ) + name = models.CharField( + max_length=125, + verbose_name=_('name'), + unique=True + ) + + class Meta: + verbose_name = _("document template") + verbose_name_plural = _("document templates") + + def __str__(self): + return str(self.name) + +@receiver(models.signals.post_delete, sender=DocumentTemplate) +def auto_delete_file_on_delete(sender, instance, **kwargs): + """ + Deletes file from filesystem + when corresponding `DocumentTemplate` object is deleted. + """ + if instance.template: + if os.path.isfile(instance.template.path): + os.remove(instance.template.path) + + +@receiver(models.signals.pre_save, sender=DocumentTemplate) +def auto_delete_file_on_change(sender, instance, **kwargs): + """ + Deletes old file from filesystem + when corresponding `DocumentTemplate` object is updated + with new file. + """ + if not instance.pk: + return False + + try: + old_file = DocumentTemplate.objects.get(pk=instance.pk).template + except DocumentTemplate.DoesNotExist: + return False + + new_file = instance.template + if not old_file == new_file: + if os.path.isfile(old_file.path): + os.remove(old_file.path) diff --git a/cotisations/templates/cotisations/aff_document_template.html b/preferences/templates/preferences/aff_document_template.html similarity index 96% rename from cotisations/templates/cotisations/aff_document_template.html rename to preferences/templates/preferences/aff_document_template.html index e35406d4..8d2184b4 100644 --- a/cotisations/templates/cotisations/aff_document_template.html +++ b/preferences/templates/preferences/aff_document_template.html @@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index f41e8397..a7d15679 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -350,6 +350,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Send voucher by email" %}{{ cotisationsoptions.send_voucher_mail | tick }} +
{% trans "Invoices' template" %} {{ cotisationsoptions.invoice_template }}{{template.template}} {% can_edit template %} - {% include 'buttons/edit.html' with href='cotisations:edit-document-template' id=template.id %} + {% include 'buttons/edit.html' with href='preferences:edit-document-template' id=template.id %} {% acl_end %} {% history_button template %}
+
+ +
+ {% can_create DocumentTemplate %} + + {% trans "Add a document template" %} + + {% acl_end %} + + {% trans "Delete one or several document templates" %} + + {% include 'preferences/aff_document_template.html' %} +
+
+

diff --git a/preferences/urls.py b/preferences/urls.py index 75ce0a8a..9bfd67d3 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -111,5 +111,20 @@ urlpatterns = [ name='edit-switchmanagementcred' ), url(r'^del_switchmanagementcred/(?P[0-9]+)$', views.del_switchmanagementcred, name='del-switchmanagementcred'), + url( + r'^add_document_template/$', + views.add_document_template, + name='add-document-template' + ), + url( + r'^edit_document_template/(?P[0-9]+)$', + views.edit_document_template, + name='edit-document-template' + ), + url( + r'^del_document_template/$', + views.del_document_template, + name='del-document-template' + ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 229b64e8..ecb3826e 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -48,7 +48,9 @@ from .forms import ( ServiceForm, ReminderForm, RadiusKeyForm, - SwitchManagementCredForm + SwitchManagementCredForm, + DocumentTemplateForm, + DelDocumentTemplateForm ) from .models import ( Service, @@ -65,6 +67,7 @@ from .models import ( SwitchManagementCred, RadiusOption, CotisationsOption, + DocumentTemplate ) from . import models from . import forms @@ -90,6 +93,7 @@ def display_options(request): switchmanagementcred_list = SwitchManagementCred.objects.all() radiusoptions, _ = RadiusOption.objects.get_or_create() cotisationsoptions, _created = CotisationsOption.objects.get_or_create() + document_template_list = DocumentTemplate.objects.order_by('name') return form({ 'useroptions': useroptions, 'machineoptions': machineoptions, @@ -105,6 +109,7 @@ def display_options(request): 'switchmanagementcred_list': switchmanagementcred_list, 'radiusoptions' : radiusoptions, 'cotisationsoptions': cotisationsoptions, + 'document_template_list': document_template_list, }, 'preferences/display_preferences.html', request) @@ -408,3 +413,86 @@ def del_mailcontact(request, instances): request ) + +@login_required +@can_create(DocumentTemplate) +def add_document_template(request): + """ + View used to add a document template. + """ + document_template = DocumentTemplateForm( + request.POST or None, + request.FILES or None, + ) + if document_template.is_valid(): + document_template.save() + messages.success( + request, + _("The document template was created.") + ) + return redirect(reverse('preferences:display-options')) + return form({ + 'preferenceform': document_template, + 'action_name': _("Add"), + 'title': _("New document template") + }, 'preferences/preferences.html', request) + + +@login_required +@can_edit(DocumentTemplate) +def edit_document_template(request, document_template_instance, **_kwargs): + """ + View used to edit a document_template. + """ + document_template = DocumentTemplateForm( + request.POST or None, + request.FILES or None, + instance=document_template_instance) + if document_template.is_valid(): + if document_template.changed_data: + document_template.save() + messages.success( + request, + _("The document template was edited.") + ) + return redirect(reverse('preferences:display-options')) + return form({ + 'preferenceform': document_template, + 'action_name': _("Edit"), + 'title': _("Edit document template") + }, 'preferences/preferences.html', request) + + +@login_required +@can_delete_set(DocumentTemplate) +def del_document_template(request, instances): + """ + View used to delete a set of document template. + """ + document_template = DelDocumentTemplateForm( + request.POST or None, instances=instances) + if document_template.is_valid(): + document_template_del = document_template.cleaned_data['document_templates'] + for document_template in document_template_del: + try: + document_template.delete() + messages.success( + request, + _("The document template %(document_template)s was deleted.") % { + 'document_template': document_template + } + ) + except ProtectedError: + messages.error( + request, + _("The document template %(document_template)s can't be deleted \ + because it is currently being used.") % { + 'document_template': document_template + } + ) + return redirect(reverse('preferences:display-options')) + return form({ + 'preferenceform': document_template, + 'action_name': _("Delete"), + 'title': _("Delete document template") + }, 'preferences/preferences.html', request) From bc758a6bcb13550519fe9038b677198fee34608c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 21 Jan 2019 09:12:06 +0100 Subject: [PATCH 15/16] Retire le lien vers les templates dans la sidebar de cotisations --- cotisations/templates/cotisations/sidebar.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 4949cd94..608f95c2 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -65,11 +65,5 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Payment methods" %} {% acl_end %} - {% can_view_all DocumentTemplate %} - - {% trans "Document templates" %} - - {% acl_end %} - {% endblock %} From 31ba9a271fc6bebe798c4b4f6e6061692ada26d8 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 21 Jan 2019 20:52:28 +0100 Subject: [PATCH 16/16] Fix get_subscription of invoice. --- cotisations/models.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index 7346a634..c6b7cd1c 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -242,14 +242,16 @@ class Facture(BaseInvoice): self.__original_control = self.control def get_subscription(self): + """Returns every subscription associated with this invoice.""" return Cotisation.objects.filter( vente__in=self.vente_set.filter( Q(type_cotisation='All') | - Q(type_cotisation='Cotisation') + Q(type_cotisation='Adhesion') ) ) def is_subscription(self): + """Returns True if this invoice contains at least one subscribtion.""" return bool(self.get_subscription()) def save(self, *args, **kwargs): @@ -257,14 +259,15 @@ class Facture(BaseInvoice): if not self.__original_valid and self.valid: send_mail_invoice(self) if self.is_subscription() \ - and not self.__original_control \ - and self.control \ - and CotisationsOption.get_cached_value('send_voucher_mail'): + and not self.__original_control \ + and self.control \ + and CotisationsOption.get_cached_value('send_voucher_mail'): send_mail_voucher(self) def __str__(self): return str(self.user) + ' ' + str(self.date) + @receiver(post_save, sender=Facture) def facture_post_save(**kwargs): """ @@ -795,7 +798,7 @@ class Paiement(RevMixin, AclMixin, models.Model): if payment_method is not None and use_payment_method: return payment_method.end_payment(invoice, request) - ## So make this invoice valid, trigger send mail + # So make this invoice valid, trigger send mail invoice.valid = True invoice.save()