diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py
index 7ba30cb..95f8c95 100644
--- a/jdav_web/finance/admin.py
+++ b/jdav_web/finance/admin.py
@@ -14,6 +14,7 @@ from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
from utils import get_member, RestrictedFileField
from rules.contrib.admin import ObjectPermissionsModelAdmin
+from members.pdf import render_tex_with_attachments
from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted, StatementConfirmed,\
StatementUnSubmitted, BillOnStatementProxy
@@ -212,6 +213,8 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
messages.success(request,
_("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.")
% {'name': str(statement)})
+ messages.success(request,
+ mark_safe(f"Hier kannst du den Abrechnungsbeleg herunterladen.")) #TODO: nice path resolution
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "confirm" in request.POST:
res = statement.validity
@@ -316,6 +319,11 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
wrap(self.unconfirm_view),
name="%s_%s_unconfirm" % (self.opts.app_label, self.opts.model_name),
),
+ path(
+ "/summary/",
+ wrap(self.statement_summary_view),
+ name="%s_%s_summary" % (self.opts.app_label, self.opts.model_name),
+ ),
]
return custom_urls + urls
@@ -342,6 +350,23 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
statement=statement)
return render(request, 'admin/unconfirm_statement.html', context=context)
+
+ def statement_summary_view(self, request, object_id):
+ statement = StatementConfirmed.objects.get(pk=object_id)
+
+ if not statement.confirmed:
+ messages.error(request,
+ _("%(name)s is not yet confirmed.") % {'name': str(statement)})
+ return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
+ excursion = statement.excursion
+ context = dict(statement=statement.template_context(), excursion=excursion, settings=settings)
+
+ pdf_filename = f"{excursion.code}_{excursion.name}_Zuschussbeleg" if excursion else f"Abrechnungsbeleg"
+ attachments = [bill.proof.path for bill in statement.bills_covered]
+ return render_tex_with_attachments(pdf_filename, 'finance/statement_summary.tex', context, attachments)
+
+ statement_summary_view.short_description = _('Download summary')
+
@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index b4ba049..13199ba 100644
--- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-01 21:11+0100\n"
+"POT-Creation-Date: 2025-03-27 23:35+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -152,6 +152,10 @@ msgstr ""
msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen"
+#: finance/admin.py
+msgid "Download summary"
+msgstr "Beleg herunterladen"
+
#: finance/apps.py
msgid "Finance"
msgstr "Finanzen"
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 72e795f..b4a770c 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -271,6 +271,11 @@ class Statement(CommonModel):
def total_bills(self):
return sum([bill.amount for bill in self.bill_set.all() if bill.costs_covered])
+ @property
+ def bills_covered(self):
+ """Returns the bills that are marked for reimbursement by the finance officer"""
+ return [bill for bill in self.bill_set.all() if bill.costs_covered]
+
@property
def total_bills_theoretic(self):
return sum([bill.amount for bill in self.bill_set.all()])
@@ -403,6 +408,7 @@ class Statement(CommonModel):
context = {
'total_bills': self.total_bills,
'total_bills_theoretic': self.total_bills_theoretic,
+ 'bills_covered': self.bills_covered,
'total': self.total,
}
if self.excursion:
@@ -424,6 +430,8 @@ class Statement(CommonModel):
'theoretical_total_staff': self.theoretical_total_staff,
'real_staff_count': self.real_staff_count,
'total_subsidies': self.total_subsidies,
+ 'subsidy_to': self.subsidy_to,
+ 'allowance_to': self.allowance_to,
}
return dict(context, **excursion_context)
else:
diff --git a/jdav_web/finance/templates/finance/statement_summary.tex b/jdav_web/finance/templates/finance/statement_summary.tex
new file mode 100644
index 0000000..3d37b32
--- /dev/null
+++ b/jdav_web/finance/templates/finance/statement_summary.tex
@@ -0,0 +1,186 @@
+{% load static common tex_extras %}
+
+\documentclass[a4paper]{article}
+
+\usepackage[utf8]{inputenc}
+% remove all undefined unicode characters instead of throwing an error
+\makeatletter
+\def\UTFviii@undefined@err#1{}
+\makeatother
+\usepackage{booktabs}
+\usepackage{amssymb}
+\usepackage{cmbright}
+\usepackage{graphicx}
+\usepackage{textpos}
+\usepackage[colorlinks, breaklinks]{hyperref}
+\usepackage{float}
+\usepackage[margin=2cm]{geometry}
+\usepackage{array}
+\usepackage{tabularx}
+\usepackage{ltablex}
+
+\newcommand{\picpos}[4]{
+ \begin{textblock*}{#1}(#2, #3)
+ \includegraphics[width=\textwidth]{#4}
+ \end{textblock*}
+}
+
+% custom url command for properly formatting emails
+\DeclareUrlCommand\Email{\urlstyle{same}}
+% allow linebreak after every character
+\expandafter\def\expandafter\UrlBreaks\expandafter{\UrlBreaks
+\do\/\do\a\do\b\do\c\do\d\do\e\do\f\do\g\do\h\do\i\do\j\do\k
+\do\l\do\m\do\n\do\o\do\p\do\q\do\r\do\s\do\t\do\u\do\v
+\do\w\do\x\do\y\do\z
+\do\A\do\B\do\C\do\D\do\E\do\F\do\G\do\H\do\I\do\J\do\K
+\do\L\do\M\do\N\do\O\do\P\do\Q\do\R\do\S\do\T\do\U\do\V
+\do\W\do\X\do\Y\do\Z}
+
+\renewcommand{\arraystretch}{1.5}
+
+\newcolumntype{L}{>{\hspace{0pt}\raggedright\arraybackslash}X}
+\newcolumntype{S}{>{\raggedright\arraybackslash\hsize=0.7\hsize}X}
+
+\newcommand{\tickedbox}{
+ \makebox[0pt][l]{$\square$}\raisebox{.15ex}{\hspace{0.1em}$\checkmark$}
+}
+\newcommand{\checkbox}{
+ \makebox[0pt][l]{$\square$}
+}
+\begin{document}
+% HEADER RIGHT
+{% settings_value 'DEFAULT_STATIC_PATH' as static_root %}
+\picpos{4.5cm}{11.5cm}{0cm}{%
+{{ static_root }}/general/img/dav_logo_sektion.png%
+}
+\begin{textblock*}{5cm}(12cm, 2.3cm)
+ \begin{flushright}
+ \small
+ \noindent Deutscher Alpenverein e. V. \\
+ Sektion {{ settings.SEKTION }} \\
+ {{ settings.SEKTION_STREET }} \\
+ {{ settings.SEKTION_TOWN }} \\
+ Tel.: {{ settings.SEKTION_TELEPHONE }} \\
+ Fax: {{ settings.SEKTION_TELEFAX }} \\
+ {{ settings.SEKTION_CONTACT_MAIL }} \\
+ \end{flushright}
+\end{textblock*}
+
+% HEADLINE
+{\noindent\LARGE{Abrechnungs- und Zuschussbeleg\\[2mm]Sektionsveranstaltung}}\\[1mm]
+\textit{Erstellt: {{ creation_date }} }\\
+
+{% if excursion %}
+\noindent\textbf{\large Ausfahrt}
+ % DESCRIPTION TABLE
+ \begin{table}[H]
+ \begin{tabular}{ll}
+ Aktivität: & {{ excursion.name|esc_all }} \\
+ Ordnungsnummer & {{ excursion.code|esc_all }} \\
+ Ort / Stützpunkt: & {{ excursion.place|esc_all }} \\
+ Zeitraum: & {{ excursion.duration|esc_all}} Tage ({{ excursion.time_period_str|esc_all }}) \\
+ Teilnehmer*innen: & {{ excursion.participant_count }} der Gruppe(n) {{ excursion.groups_str|esc_all }} \\
+ Betreuer*innen: & {{excursion.staff_count|esc_all }} ({{ excursion.staff_str|esc_all }}) \\
+ Art der Tour: & {% checked_if_true 'Gemeinschaftstour' excursion.get_tour_type %}
+ {% checked_if_true 'Führungstour' excursion.get_tour_type %}
+ {% checked_if_true 'Ausbildung' excursion.get_tour_type %} \\
+ Anreise: & {% checked_if_true 'ÖPNV' excursion.get_tour_approach %}
+ {% checked_if_true 'Muskelkraft' excursion.get_tour_approach %}
+ {% checked_if_true 'Fahrgemeinschaften' excursion.get_tour_approach %}
+ \end{tabular}
+ \end{table}
+
+\noindent\textbf{\large Zuschüsse und Aufwandsentschädigung}
+{% if excursion.approved_staff_count > 0 %}
+ \noindent Gemäß Beschluss des Jugendausschusses gelten folgende Sätze für Zuschüsse pro genehmigter Jugendleiter*in:
+
+ \begin{table}[H]
+ \centering
+ \begin{tabular}{lllr}
+ \toprule
+ \textbf{Posten} & \textbf{Einzelsatz} & \textbf{Anzahl} & \textbf{Gesamtbetrag pro JL} \\
+ \midrule
+ Zuschuss Übernachtung & {{ statement.price_per_night }} € / Nacht & {{ statement.nights }} Nächte & {{ statement.nights_per_yl }} € \\
+ Zuschuss Anreise & {{statement.euro_per_km}} € / km ({{ statement.means_of_transport }}) & {{ statement.kilometers_traveled }} km & {{ statement.transportation_per_yl }} € \\
+ Aufwandsentschädigung & {{ statement.allowance_per_day }},00 € / Tag & {{ statement.duration }} Tage & {{ statement.allowance_per_yl }} € \\
+ \midrule
+ \textbf{Summe}& & & \textbf{ {{ statement.total_per_yl }} }€\\
+ \bottomrule
+ \end{tabular}
+ \end{table}
+
+\noindent Gemäß JDAV-Betreuungsschlüssel können bei {{ excursion.participant_count }} Teilnehmer*innen
+bis zu {{ excursion.approved_staff_count }} Jugendleiter*innen {% if excursion.approved_extra_youth_leader_count %}
+(davon {{ excursion.approved_extra_youth_leader_count }} durch das Jugendreferat zusätzlich genehmigt){% endif %} bezuschusst werden.
+Zuschüsse und Aufwandsentschädigung werden wie folgt abgerufen:
+\begin{itemize}
+
+ {% if statement.allowances_paid > 0 %}
+
+ \item Eine Aufwandsentschädigung von {{ statement.allowance_per_yl }} € pro Jugendleiter*in wird überwiesen an:
+ {% for m in statement.allowance_to.all %}{% if forloop.counter > 1 %}, {% endif %}{{ m.name }}{% endfor %}
+ {% else %}
+ \item Keiner*r der Jugendleiter*innen nimmt eine Aufwandsentschädigung in Anspruch.
+ {% endif %}
+
+ {% if statement.subsidy_to %}
+ \item Der Zuschuss zu Übernachtung und Anreise für alle Jugendleiter*innen in Höhe von {{ statement.total_subsidies }} € wird überwiesen an:
+ {{ statement.subsidy_to.name }}
+
+ {% else %}
+ \item Zuschüsse zu Übernachtung und Anreise werden nicht in Anspruch genommen.
+ {% endif %}
+
+\end{itemize}
+{% else %}
+\noindent Für die vorliegende Ausfahrt sind keine Jugendleiter*innen anspruchsberechtigt für Zuschüsse oder Aufwandsentschädigung.
+{% endif %}
+
+{% else %}
+\vspace{110pt}
+{% endif %}
+
+
+\vspace{12pt}
+
+\noindent\textbf{\large Ausgabenübersicht}
+\nopagebreak
+\begin{table}[H]
+ \centering
+ \begin{tabular}{lllr}
+ \toprule
+ \textbf{Titel} & \textbf{Beschreibung} & \textbf{Auszahlung an} & \textbf{Betrag} \\
+ \midrule
+
+{% if statement.bills_covered %}
+ {% for bill in statement.bills_covered %}
+ {{ forloop.counter }}. {{ bill.short_description}} & {{ bill.explanation}} & {{ bill.paid_by.name }} & {{ bill.amount }} € \\
+ {% endfor %}
+ \midrule
+ \multicolumn{3}{l}{\textbf{Summe übernommene Ausgaben}} & \textbf{ {{ statement.total_bills }} }€\\
+{% endif %}
+{% if excursion.approved_staff_count > 0 and statement.allowances_paid > 0 or excursion.approved_staff_count > 0 and statement.subsidy_to %}
+ \midrule
+ {% if statement.allowances_paid > 0 %}
+ {% for m in statement.allowance_to.all %}
+ Aufwandsentschädigung & & {{ m.name }} & {{ statement.allowance_per_yl }} €\\
+ {% endfor %}
+ {% endif %}
+ {% if statement.subsidy_to %}
+ \multicolumn{2}{l}{Zuschuss Übernachtung und Anreise für alle Jugendleiter*innen} & {{ statement.subsidy_to.name }} & {{ statement.total_subsidies}} €\\
+ {% endif %}
+ \midrule
+ \multicolumn{3}{l}{\textbf{Summe Zuschüsse und Aufwandsentschädigung}} & \textbf{ {{ statement.total_staff }} }€\\
+{%endif %}
+{% if statement.bills_covered and excursion.approved_staff_count > 0 %}
+ \midrule
+ \textbf{Gesamtsumme}& & & \textbf{ {{ statement.total }} }€\\
+{% endif %}
+ \bottomrule
+ \end{tabular}
+\end{table}
+
+\noindent Dieser Beleg wird automatisch erstellt und daher nicht unterschrieben.
+
+
+\end{document}
diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po
index cafea14..e32540b 100644
--- a/jdav_web/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-01 14:54+0100\n"
+"POT-Creation-Date: 2025-03-27 23:35+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -236,6 +236,10 @@ msgstr "Löschen?"
msgid "Unconfirm"
msgstr "Bestätigung zurücknehmen"
+#: templates/admin/finance/statementconfirmed/change_form_object_tools.html
+msgid "Download summary"
+msgstr "Beleg herunterladen"
+
#: templates/admin/finance/statementsubmitted/change_form_object_tools.html
msgid "Reduce transactions"
msgstr "Überweisungen minimieren"
diff --git a/jdav_web/members/pdf.py b/jdav_web/members/pdf.py
index 851d79c..f3a2d15 100644
--- a/jdav_web/members/pdf.py
+++ b/jdav_web/members/pdf.py
@@ -47,6 +47,23 @@ def render_docx(name, template_path, context, save_only=False):
return filename_docx
return serve_media(filename_docx, 'application/docx')
+def render_tex_with_attachments(name, template_path, context, attachments, save_only=False):
+
+ rendered_pdf = render_tex(name, template_path, context, save_only=True)
+
+ reader = PdfReader(media_path(rendered_pdf))
+ writer = PdfWriter()
+ writer.append(reader)
+
+ pdf_add_attachments(writer, attachments)
+
+ with open(media_path(rendered_pdf), 'wb') as output_stream:
+ writer.write(output_stream)
+
+ if save_only:
+ return rendered_pdf
+ return serve_pdf(rendered_pdf)
+
def render_tex(name, template_path, context, save_only=False):
filename = generate_tex(name, template_path, context)
@@ -73,6 +90,27 @@ def render_tex(name, template_path, context, save_only=False):
return serve_pdf(filename_pdf)
+def pdf_add_attachments(pdf_writer, attachments):
+ for fp in attachments:
+ try:
+ if fp.endswith(".pdf"):
+ # append pdf directly
+ img_pdf = PdfReader(fp)
+ else:
+ # convert ensures that png files with an alpha channel can be appended
+ img = Image.open(fp).convert("RGB")
+ img_io = BytesIO()
+ img.save(img_io, "pdf")
+ img_io.seek(0)
+ img_pdf = PdfReader(img_io)
+ img_pdf_scaled = scale_pdf_to_a4(img_pdf)
+ pdf_writer.append(img_pdf_scaled)
+
+ except Exception as e:
+ print("Could not add image", fp)
+ print(e)
+
+
def scale_pdf_page_to_a4(page):
A4_WIDTH, A4_HEIGHT = 595, 842
@@ -112,23 +150,7 @@ def fill_pdf_form(name, template_path, fields, attachments=[], save_only=False):
writer.update_page_form_field_values(None, fields, auto_regenerate=False)
- for fp in attachments:
- try:
- if fp.endswith(".pdf"):
- # append pdf directly
- img_pdf = PdfReader(fp)
- else:
- # convert ensures that png files with an alpha channel can be appended
- img = Image.open(fp).convert("RGB")
- img_io = BytesIO()
- img.save(img_io, "pdf")
- img_io.seek(0)
- img_pdf = PdfReader(img_io)
- img_pdf_scaled = scale_pdf_to_a4(img_pdf)
- writer.append(img_pdf_scaled)
- except Exception as e:
- print("Could not add image", fp)
- print(e)
+ pdf_add_attachments(writer, attachments)
with open(media_path(filename_pdf), 'wb') as output_stream:
writer.write(output_stream)
diff --git a/jdav_web/templates/admin/finance/statementconfirmed/change_form_object_tools.html b/jdav_web/templates/admin/finance/statementconfirmed/change_form_object_tools.html
index 1906f52..0ab5d39 100644
--- a/jdav_web/templates/admin/finance/statementconfirmed/change_form_object_tools.html
+++ b/jdav_web/templates/admin/finance/statementconfirmed/change_form_object_tools.html
@@ -7,7 +7,10 @@
{% url opts|admin_urlname:'unconfirm' original.pk|admin_urlquote as invite_url %}
{% trans 'Unconfirm' %}
-
+
+ {% url opts|admin_urlname:'summary' original.pk|admin_urlquote as invite_url %}
+ {% trans 'Download summary' %}
+
{{block.super}}
{% endblock %}