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 %}
+
+
+
+
+ {% trans "Document template" %} |
+ {% trans "File" %} |
+ |
+
+
+ {% for template in document_template_list %}
+
+ {{ 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 %}
+ |
+
+ {% endfor %}
+
+
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 )