mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-23 11:53:12 +00:00
Merge branch 'feature_document_templates' into 'dev'
Feature document templates See merge request federez/re2o!391
This commit is contained in:
commit
411467a520
22 changed files with 741 additions and 49 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -164,3 +164,17 @@ Collec new statics
|
||||||
```bash
|
```bash
|
||||||
python3 manage.py collectstatic
|
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.
|
||||||
|
|
|
@ -611,9 +611,11 @@ class HostMacIpView(generics.ListAPIView):
|
||||||
"""Exposes the associations between hostname, mac address and IPv4 in
|
"""Exposes the associations between hostname, mac address and IPv4 in
|
||||||
order to build the DHCP lease files.
|
order to build the DHCP lease files.
|
||||||
"""
|
"""
|
||||||
queryset = all_active_interfaces()
|
|
||||||
serializer_class = serializers.HostMacIpSerializer
|
serializer_class = serializers.HostMacIpSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return all_active_interfaces()
|
||||||
|
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
from .models import (
|
from .models import (
|
||||||
Article, Paiement, Facture, Banque,
|
Article, Paiement, Facture, Banque,
|
||||||
CustomInvoice, Vente, CostEstimate
|
CustomInvoice, Vente, CostEstimate,
|
||||||
)
|
)
|
||||||
from .payment_methods import balance
|
from .payment_methods import balance
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,14 @@ from django.urls import reverse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
|
from preferences.models import CotisationsOption
|
||||||
from machines.models import regen
|
from machines.models import regen
|
||||||
from re2o.field_permissions import FieldPermissionModelMixin
|
from re2o.field_permissions import FieldPermissionModelMixin
|
||||||
from re2o.mixins import AclMixin, RevMixin
|
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
|
from cotisations.validators import check_no_balance
|
||||||
|
|
||||||
|
|
||||||
|
@ -236,15 +239,35 @@ class Facture(BaseInvoice):
|
||||||
'control': self.can_change_control,
|
'control': self.can_change_control,
|
||||||
}
|
}
|
||||||
self.__original_valid = self.valid
|
self.__original_valid = self.valid
|
||||||
|
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='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):
|
def save(self, *args, **kwargs):
|
||||||
super(Facture, self).save(*args, **kwargs)
|
super(Facture, self).save(*args, **kwargs)
|
||||||
if not self.__original_valid and self.valid:
|
if not self.__original_valid and self.valid:
|
||||||
send_mail_invoice(self)
|
send_mail_invoice(self)
|
||||||
|
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):
|
def __str__(self):
|
||||||
return str(self.user) + ' ' + str(self.date)
|
return str(self.user) + ' ' + str(self.date)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Facture)
|
@receiver(post_save, sender=Facture)
|
||||||
def facture_post_save(**kwargs):
|
def facture_post_save(**kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -775,7 +798,7 @@ class Paiement(RevMixin, AclMixin, models.Model):
|
||||||
if payment_method is not None and use_payment_method:
|
if payment_method is not None and use_payment_method:
|
||||||
return payment_method.end_payment(invoice, request)
|
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.valid = True
|
||||||
invoice.save()
|
invoice.save()
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if estimate.final_invoice %}
|
{% if estimate.final_invoice %}
|
||||||
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
|
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i style="color: #D10115;" class="fa fa-times"></i>'
|
<i style="color: #D10115;" class="fa fa-times"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -48,7 +48,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% trans "Date" as tr_date %}
|
{% trans "Date" as tr_date %}
|
||||||
{% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %}
|
{% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %}
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Invoice ID" as tr_invoice_id %}
|
{% trans "Invoice ID" as tr_invoice_id %}
|
||||||
{% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %}
|
{% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %}
|
||||||
</th>
|
</th>
|
||||||
|
@ -63,17 +63,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<td>{{ facture.prix_total }}</td>
|
<td>{{ facture.prix_total }}</td>
|
||||||
<td>{{ facture.paiement }}</td>
|
<td>{{ facture.paiement }}</td>
|
||||||
<td>{{ facture.date }}</td>
|
<td>{{ facture.date }}</td>
|
||||||
<td>{{ facture.id }}</td>
|
<td>{{ facture.id }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% can_edit facture %}
|
{% 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 %}
|
{% acl_else %}
|
||||||
{% trans "Controlled invoice" %}
|
{% trans "Controlled invoice" %}
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
{% can_delete facture %}
|
{% 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 %}
|
{% acl_end %}
|
||||||
{% history_button facture %}
|
{% history_button facture %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if facture.valid %}
|
{% if facture.valid %}
|
||||||
|
@ -83,13 +83,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="text-danger">{% trans "Invalidated invoice" %}</i>
|
<i class="text-danger">{% trans "Invalidated invoice" %}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if facture.control and facture.is_subscription %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:voucher-pdf' facture.id %}">
|
||||||
|
<i class="fa fa-file-pdf-o"></i> {% trans "Voucher" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% if facture_list.paginator %}
|
{% if facture_list.paginator %}
|
||||||
{% include 'pagination.html' with list=facture_list %}
|
{% include 'pagination.html' with list=facture_list %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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.
|
87
cotisations/templates/cotisations/voucher.tex
Normal file
87
cotisations/templates/cotisations/voucher.tex
Normal file
|
@ -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 %}
|
|
@ -31,12 +31,16 @@ from subprocess import Popen, PIPE
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
import logging
|
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-')
|
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
|
||||||
|
@ -49,6 +53,7 @@ def render_invoice(_request, ctx={}):
|
||||||
Render an invoice using some available information such as the current
|
Render an invoice using some available information such as the current
|
||||||
date, the user, the articles, the prices, ...
|
date, the user, the articles, the prices, ...
|
||||||
"""
|
"""
|
||||||
|
options, _ = CotisationsOption.objects.get_or_create()
|
||||||
is_estimate = ctx.get('is_estimate', False)
|
is_estimate = ctx.get('is_estimate', False)
|
||||||
filename = '_'.join([
|
filename = '_'.join([
|
||||||
'cost_estimate' if is_estimate else 'invoice',
|
'cost_estimate' if is_estimate else 'invoice',
|
||||||
|
@ -58,7 +63,30 @@ def render_invoice(_request, ctx={}):
|
||||||
str(ctx.get('DATE', datetime.now()).month),
|
str(ctx.get('DATE', datetime.now()).month),
|
||||||
str(ctx.get('DATE', datetime.now()).day),
|
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
|
||||||
|
)
|
||||||
|
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('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 = render_tex(_request, templatename, ctx)
|
||||||
r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format(
|
r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format(
|
||||||
name=filename
|
name=filename
|
||||||
)
|
)
|
||||||
|
@ -83,12 +111,13 @@ def create_pdf(template, ctx={}):
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tempdir:
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
process = Popen(
|
with open("/var/www/re2o/out.log", "w") as f:
|
||||||
['pdflatex', '-output-directory', tempdir],
|
process = Popen(
|
||||||
stdin=PIPE,
|
['pdflatex', '-output-directory', tempdir],
|
||||||
stdout=PIPE,
|
stdin=PIPE,
|
||||||
)
|
stdout=f,#PIPE,
|
||||||
process.communicate(rendered_tpl)
|
)
|
||||||
|
process.communicate(rendered_tpl)
|
||||||
with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f:
|
with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f:
|
||||||
pdf = f.read()
|
pdf = f.read()
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,11 @@ urlpatterns = [
|
||||||
views.facture_pdf,
|
views.facture_pdf,
|
||||||
name='facture-pdf'
|
name='facture-pdf'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^voucher_pdf/(?P<factureid>[0-9]+)$',
|
||||||
|
views.voucher_pdf,
|
||||||
|
name='voucher-pdf'
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'^new_cost_estimate/$',
|
r'^new_cost_estimate/$',
|
||||||
views.new_cost_estimate,
|
views.new_cost_estimate,
|
||||||
|
@ -176,5 +181,5 @@ urlpatterns = [
|
||||||
views.control,
|
views.control,
|
||||||
name='control'
|
name='control'
|
||||||
),
|
),
|
||||||
url(r'^$', views.index, name='index'),
|
url(r'^$', views.index, name='index'),
|
||||||
] + payment_methods.urls.urlpatterns
|
] + payment_methods.urls.urlpatterns
|
||||||
|
|
|
@ -25,7 +25,7 @@ from django.template.loader import get_template
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
|
|
||||||
from .tex import create_pdf
|
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.settings import LOGO_PATH
|
||||||
from re2o import settings
|
from re2o import settings
|
||||||
|
|
||||||
|
@ -93,3 +93,38 @@ def send_mail_invoice(invoice):
|
||||||
attachments=[('invoice.pdf', pdf, 'application/pdf')]
|
attachments=[('invoice.pdf', pdf, 'application/pdf')]
|
||||||
)
|
)
|
||||||
mail.send()
|
mail.send()
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail_voucher(invoice):
|
||||||
|
"""Creates a voucher from an invoice and sends it by email to the client"""
|
||||||
|
ctx = {
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
),
|
||||||
|
'asso_email': AssoOption.get_cached_value('contact'),
|
||||||
|
'asso_name': AssoOption.get_cached_value('name')
|
||||||
|
}
|
||||||
|
|
||||||
|
mail = EmailMessage(
|
||||||
|
'Votre reçu / Your voucher',
|
||||||
|
template.render(ctx),
|
||||||
|
GeneralOption.get_cached_value('email_from'),
|
||||||
|
[invoice.user.get_mail],
|
||||||
|
attachments=[('voucher.pdf', pdf, 'application/pdf')]
|
||||||
|
)
|
||||||
|
mail.send()
|
||||||
|
|
|
@ -69,7 +69,7 @@ from .models import (
|
||||||
Banque,
|
Banque,
|
||||||
CustomInvoice,
|
CustomInvoice,
|
||||||
BaseInvoice,
|
BaseInvoice,
|
||||||
CostEstimate
|
CostEstimate,
|
||||||
)
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
FactureForm,
|
FactureForm,
|
||||||
|
@ -85,7 +85,7 @@ from .forms import (
|
||||||
DiscountForm,
|
DiscountForm,
|
||||||
CostEstimateForm,
|
CostEstimateForm,
|
||||||
)
|
)
|
||||||
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 .payment_methods.forms import payment_method_factory
|
||||||
from .utils import find_payment_method
|
from .utils import find_payment_method
|
||||||
|
|
||||||
|
@ -217,6 +217,7 @@ def new_cost_estimate(request):
|
||||||
number=quantity
|
number=quantity
|
||||||
)
|
)
|
||||||
discount_form.apply_to_invoice(cost_estimate_instance)
|
discount_form.apply_to_invoice(cost_estimate_instance)
|
||||||
|
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("The cost estimate was created.")
|
_("The cost estimate was created.")
|
||||||
|
@ -482,7 +483,6 @@ def cost_estimate_pdf(request, invoice, **_kwargs):
|
||||||
invoice with the total price, the payment method, the address and the
|
invoice with the total price, the payment method, the address and the
|
||||||
legal information for the user.
|
legal information for the user.
|
||||||
"""
|
"""
|
||||||
# TODO : change vente to purchase
|
|
||||||
purchases_objects = Vente.objects.all().filter(facture=invoice)
|
purchases_objects = Vente.objects.all().filter(facture=invoice)
|
||||||
# Get the article list and build an list out of it
|
# Get the article list and build an list out of it
|
||||||
# contiaining (article_name, article_price, quantity, total_price)
|
# contiaining (article_name, article_price, quantity, total_price)
|
||||||
|
@ -720,7 +720,7 @@ def edit_paiement(request, paiement_instance, **_kwargs):
|
||||||
if payment_method is not None:
|
if payment_method is not None:
|
||||||
payment_method.save()
|
payment_method.save()
|
||||||
messages.success(
|
messages.success(
|
||||||
request,_("The payment method was edited.")
|
request, _("The payment method was edited.")
|
||||||
)
|
)
|
||||||
return redirect(reverse('cotisations:index-paiement'))
|
return redirect(reverse('cotisations:index-paiement'))
|
||||||
return form({
|
return form({
|
||||||
|
@ -954,7 +954,8 @@ def index_custom_invoice(request):
|
||||||
"""View used to display every custom invoice."""
|
"""View used to display every custom invoice."""
|
||||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
|
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 = SortTable.sort(
|
||||||
custom_invoice_list,
|
custom_invoice_list,
|
||||||
request.GET.get('col'),
|
request.GET.get('col'),
|
||||||
|
@ -1020,7 +1021,8 @@ def credit_solde(request, user, **_kwargs):
|
||||||
kwargs={'userid': user.id}
|
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():
|
if refill_form.is_valid():
|
||||||
price = refill_form.cleaned_data['value']
|
price = refill_form.cleaned_data['value']
|
||||||
invoice = Facture(user=user)
|
invoice = Facture(user=user)
|
||||||
|
@ -1050,3 +1052,29 @@ def credit_solde(request, user, **_kwargs):
|
||||||
'max_balance': find_payment_method(p).maximum_balance,
|
'max_balance': find_payment_method(p).maximum_balance,
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@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.date
|
||||||
|
})
|
||||||
|
|
|
@ -324,6 +324,21 @@ 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/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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
create_superuser() {
|
create_superuser() {
|
||||||
### Usage: create_superuser
|
### Usage: create_superuser
|
||||||
#
|
#
|
||||||
|
@ -748,9 +763,10 @@ main_function() {
|
||||||
echo " * {help} ---------- Display this quick usage documentation"
|
echo " * {help} ---------- Display this quick usage documentation"
|
||||||
echo " * {setup} --------- Launch the full interactive guide to setup entirely"
|
echo " * {setup} --------- Launch the full interactive guide to setup entirely"
|
||||||
echo " re2o from scratch"
|
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 " and pip packages and apply the migrations to the DB"
|
||||||
echo " * {update-django} - Apply Django migration and collect frontend statics"
|
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-packages} Install the missing APT and pip packages"
|
||||||
echo " * {update-settings} Interactively rewrite the settings file"
|
echo " * {update-settings} Interactively rewrite the settings file"
|
||||||
echo " * {reset-db} ------ Erase the previous local database, setup a new empty"
|
echo " * {reset-db} ------ Erase the previous local database, setup a new empty"
|
||||||
|
@ -782,9 +798,14 @@ main_function() {
|
||||||
|
|
||||||
update )
|
update )
|
||||||
install_requirements
|
install_requirements
|
||||||
|
copy_templates_files
|
||||||
update_django
|
update_django
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
copy-templates-files )
|
||||||
|
copy_templates_files
|
||||||
|
;;
|
||||||
|
|
||||||
update-django )
|
update-django )
|
||||||
update_django
|
update_django
|
||||||
;;
|
;;
|
||||||
|
|
|
@ -40,7 +40,8 @@ from .models import (
|
||||||
HomeOption,
|
HomeOption,
|
||||||
RadiusKey,
|
RadiusKey,
|
||||||
SwitchManagementCred,
|
SwitchManagementCred,
|
||||||
Reminder
|
Reminder,
|
||||||
|
DocumentTemplate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +102,12 @@ class ReminderAdmin(VersionAdmin):
|
||||||
"""Class reminder for switch"""
|
"""Class reminder for switch"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentTemplateAdmin(VersionAdmin):
|
||||||
|
"""Admin class for DocumentTemplate"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(OptionalUser, OptionalUserAdmin)
|
admin.site.register(OptionalUser, OptionalUserAdmin)
|
||||||
admin.site.register(OptionalMachine, OptionalMachineAdmin)
|
admin.site.register(OptionalMachine, OptionalMachineAdmin)
|
||||||
admin.site.register(OptionalTopologie, OptionalTopologieAdmin)
|
admin.site.register(OptionalTopologie, OptionalTopologieAdmin)
|
||||||
|
@ -113,3 +120,4 @@ admin.site.register(RadiusKey, RadiusKeyAdmin)
|
||||||
admin.site.register(SwitchManagementCred, SwitchManagementCredAdmin)
|
admin.site.register(SwitchManagementCred, SwitchManagementCredAdmin)
|
||||||
admin.site.register(AssoOption, AssoOptionAdmin)
|
admin.site.register(AssoOption, AssoOptionAdmin)
|
||||||
admin.site.register(MailMessageOption, MailMessageOptionAdmin)
|
admin.site.register(MailMessageOption, MailMessageOptionAdmin)
|
||||||
|
admin.site.register(DocumentTemplate, DocumentTemplateAdmin)
|
||||||
|
|
|
@ -43,9 +43,12 @@ from .models import (
|
||||||
RadiusKey,
|
RadiusKey,
|
||||||
SwitchManagementCred,
|
SwitchManagementCred,
|
||||||
RadiusOption,
|
RadiusOption,
|
||||||
|
CotisationsOption,
|
||||||
|
DocumentTemplate
|
||||||
)
|
)
|
||||||
from topologie.models import Switch
|
from topologie.models import Switch
|
||||||
|
|
||||||
|
|
||||||
class EditOptionalUserForm(ModelForm):
|
class EditOptionalUserForm(ModelForm):
|
||||||
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
|
"""Formulaire d'édition des options de l'user. (solde, telephone..)"""
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -182,9 +185,6 @@ class EditAssoOptionForm(ModelForm):
|
||||||
self.fields['pseudo'].label = _("Usual name")
|
self.fields['pseudo'].label = _("Usual name")
|
||||||
self.fields['utilisateur_asso'].label = _("Account used for editing"
|
self.fields['utilisateur_asso'].label = _("Account used for editing"
|
||||||
" from /admin")
|
" 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")
|
self.fields['description'].label = _("Description")
|
||||||
|
|
||||||
|
|
||||||
|
@ -253,6 +253,13 @@ class EditRadiusOptionForm(ModelForm):
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class EditCotisationsOptionForm(ModelForm):
|
||||||
|
"""Edition forms for Cotisations options"""
|
||||||
|
class Meta:
|
||||||
|
model = CotisationsOption
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class ServiceForm(ModelForm):
|
class ServiceForm(ModelForm):
|
||||||
"""Edition, ajout de services sur la page d'accueil"""
|
"""Edition, ajout de services sur la page d'accueil"""
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -371,3 +378,36 @@ class DelMailContactForm(Form):
|
||||||
else:
|
else:
|
||||||
self.fields['mailcontacts'].queryset = MailContact.objects.all()
|
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()
|
||||||
|
|
62
preferences/migrations/0059_auto_20190120_1739.py
Normal file
62
preferences/migrations/0059_auto_20190120_1739.py
Normal file
|
@ -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),
|
||||||
|
]
|
|
@ -24,6 +24,7 @@
|
||||||
Reglages généraux, machines, utilisateurs, mail, general pour l'application.
|
Reglages généraux, machines, utilisateurs, mail, general pour l'application.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import os
|
||||||
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -36,7 +37,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import machines.models
|
import machines.models
|
||||||
|
|
||||||
from re2o.mixins import AclMixin
|
from re2o.mixins import AclMixin, RevMixin
|
||||||
from re2o.aes_field import AESEncryptedField
|
from re2o.aes_field import AESEncryptedField
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -521,6 +522,12 @@ class AssoOption(AclMixin, PreferencesModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=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:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -687,3 +694,95 @@ class RadiusOption(AclMixin, PreferencesModel):
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def default_invoice():
|
||||||
|
tpl, _ = DocumentTemplate.objects.get_or_create(
|
||||||
|
name="Re2o default invoice",
|
||||||
|
template="templates/default_invoice.tex"
|
||||||
|
)
|
||||||
|
return tpl.id
|
||||||
|
|
||||||
|
|
||||||
|
def default_voucher():
|
||||||
|
tpl, _ = 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")
|
||||||
|
|
||||||
|
invoice_template = models.OneToOneField(
|
||||||
|
'preferences.DocumentTemplate',
|
||||||
|
verbose_name=_("Template for invoices"),
|
||||||
|
related_name="invoice_template",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
default=default_invoice,
|
||||||
|
)
|
||||||
|
voucher_template = models.OneToOneField(
|
||||||
|
'preferences.DocumentTemplate',
|
||||||
|
verbose_name=_("Template for subscription voucher"),
|
||||||
|
related_name="voucher_template",
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
50
preferences/templates/preferences/aff_document_template.html
Normal file
50
preferences/templates/preferences/aff_document_template.html
Normal file
|
@ -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 %}
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Document template" %}</th>
|
||||||
|
<th>{% trans "File" %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for template in document_template_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ template.name }}</td>
|
||||||
|
<td><a href="{{template.template.url}}">{{template.template}}</a></td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% can_edit template %}
|
||||||
|
{% include 'buttons/edit.html' with href='preferences:edit-document-template' id=template.id %}
|
||||||
|
{% acl_end %}
|
||||||
|
{% history_button template %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
|
@ -343,9 +343,59 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<th>{% trans "Description of the organisation" %}</th>
|
<th>{% trans "Description of the organisation" %}</th>
|
||||||
<td>{{ assooptions.description|safe }}</td>
|
<td>{{ assooptions.description|safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "President of the association"%}</th>
|
||||||
|
<td>{{ assooptions.pres_name }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default" id="templates">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_templates">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-edit"></i> {% trans "Document templates" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_templates" class="panel-collapse panel-body collapse">
|
||||||
|
{% can_create DocumentTemplate %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-document-template' %}">
|
||||||
|
<i class="fa fa-cart-plus"></i> {% trans "Add a document template" %}
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
|
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-document-template' %}">
|
||||||
|
<i class="fa fa-trash"></i> {% trans "Delete one or several document templates" %}
|
||||||
|
</a>
|
||||||
|
{% include 'preferences/aff_document_template.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="cotisation">
|
||||||
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_cotisation">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a><i class="fa fa-eur"></i> {% trans "Cotisation's options" %}</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse_cotisation" class="panel-collapse panel-body collapse">
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'CotisationsOption' %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Send voucher by email" %}</th>
|
||||||
|
<td>{{ cotisationsoptions.send_voucher_mail | tick }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Invoices' template" %}</th>
|
||||||
|
<td>{{ cotisationsoptions.invoice_template }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Vouchers' template" %}</th>
|
||||||
|
<td>{{ cotisationsoptions.voucher_template }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="panel panel-default" id="mail">
|
<div class="panel panel-default" id="mail">
|
||||||
<div class="panel-heading" data-toggle="collapse" href="#collapse_mail">
|
<div class="panel-heading" data-toggle="collapse" href="#collapse_mail">
|
||||||
|
|
|
@ -71,6 +71,11 @@ urlpatterns = [
|
||||||
views.edit_options,
|
views.edit_options,
|
||||||
name='edit-options'
|
name='edit-options'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^edit_options/(?P<section>CotisationsOption)$',
|
||||||
|
views.edit_options,
|
||||||
|
name='edit-options'
|
||||||
|
),
|
||||||
url(r'^add_service/$', views.add_service, name='add-service'),
|
url(r'^add_service/$', views.add_service, name='add-service'),
|
||||||
url(
|
url(
|
||||||
r'^edit_service/(?P<serviceid>[0-9]+)$',
|
r'^edit_service/(?P<serviceid>[0-9]+)$',
|
||||||
|
@ -106,5 +111,20 @@ urlpatterns = [
|
||||||
name='edit-switchmanagementcred'
|
name='edit-switchmanagementcred'
|
||||||
),
|
),
|
||||||
url(r'^del_switchmanagementcred/(?P<switchmanagementcredid>[0-9]+)$', views.del_switchmanagementcred, name='del-switchmanagementcred'),
|
url(r'^del_switchmanagementcred/(?P<switchmanagementcredid>[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<documenttemplateid>[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'),
|
url(r'^$', views.display_options, name='display-options'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -48,7 +48,9 @@ from .forms import (
|
||||||
ServiceForm,
|
ServiceForm,
|
||||||
ReminderForm,
|
ReminderForm,
|
||||||
RadiusKeyForm,
|
RadiusKeyForm,
|
||||||
SwitchManagementCredForm
|
SwitchManagementCredForm,
|
||||||
|
DocumentTemplateForm,
|
||||||
|
DelDocumentTemplateForm
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
Service,
|
Service,
|
||||||
|
@ -64,6 +66,8 @@ from .models import (
|
||||||
RadiusKey,
|
RadiusKey,
|
||||||
SwitchManagementCred,
|
SwitchManagementCred,
|
||||||
RadiusOption,
|
RadiusOption,
|
||||||
|
CotisationsOption,
|
||||||
|
DocumentTemplate
|
||||||
)
|
)
|
||||||
from . import models
|
from . import models
|
||||||
from . import forms
|
from . import forms
|
||||||
|
@ -88,6 +92,8 @@ def display_options(request):
|
||||||
radiuskey_list = RadiusKey.objects.all()
|
radiuskey_list = RadiusKey.objects.all()
|
||||||
switchmanagementcred_list = SwitchManagementCred.objects.all()
|
switchmanagementcred_list = SwitchManagementCred.objects.all()
|
||||||
radiusoptions, _ = RadiusOption.objects.get_or_create()
|
radiusoptions, _ = RadiusOption.objects.get_or_create()
|
||||||
|
cotisationsoptions, _created = CotisationsOption.objects.get_or_create()
|
||||||
|
document_template_list = DocumentTemplate.objects.order_by('name')
|
||||||
return form({
|
return form({
|
||||||
'useroptions': useroptions,
|
'useroptions': useroptions,
|
||||||
'machineoptions': machineoptions,
|
'machineoptions': machineoptions,
|
||||||
|
@ -102,6 +108,8 @@ def display_options(request):
|
||||||
'radiuskey_list' : radiuskey_list,
|
'radiuskey_list' : radiuskey_list,
|
||||||
'switchmanagementcred_list': switchmanagementcred_list,
|
'switchmanagementcred_list': switchmanagementcred_list,
|
||||||
'radiusoptions' : radiusoptions,
|
'radiusoptions' : radiusoptions,
|
||||||
|
'cotisationsoptions': cotisationsoptions,
|
||||||
|
'document_template_list': document_template_list,
|
||||||
}, 'preferences/display_preferences.html', request)
|
}, 'preferences/display_preferences.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -405,3 +413,86 @@ def del_mailcontact(request, instances):
|
||||||
request
|
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)
|
||||||
|
|
|
@ -120,6 +120,7 @@ TEMPLATES = [
|
||||||
'DIRS': [
|
'DIRS': [
|
||||||
# Use only absolute paths with '/' delimiters even on Windows
|
# Use only absolute paths with '/' delimiters even on Windows
|
||||||
os.path.join(BASE_DIR, 'templates').replace('\\', '/'),
|
os.path.join(BASE_DIR, 'templates').replace('\\', '/'),
|
||||||
|
os.path.join(BASE_DIR, 'media', 'templates').replace('\\', '/'),
|
||||||
],
|
],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
|
|
Loading…
Reference in a new issue