From 6fdf8a0406c15b7aa08a764d1e027a7c77ae0238 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 Jan 2019 19:52:06 +0100 Subject: [PATCH] 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 )