From 54e424d25b34becd262ff34a5b1bbac531566e24 Mon Sep 17 00:00:00 2001
From: mariusrklein
Date: Thu, 12 Feb 2026 00:17:56 +0100
Subject: [PATCH 1/6] fix(excursion): more logical order and position of fields
and more help texts
---
jdav_web/finance/locale/de/LC_MESSAGES/django.po | 14 ++++++++++++--
jdav_web/finance/models.py | 9 ++++++++-
jdav_web/members/admin.py | 7 +++----
jdav_web/members/locale/de/LC_MESSAGES/django.po | 16 +++++++++++++---
jdav_web/members/models/excursion.py | 11 ++++++++++-
5 files changed, 46 insertions(+), 11 deletions(-)
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index 1606d561..8e26e899 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: 2026-01-18 11:05+0100\n"
+"POT-Creation-Date: 2026-02-12 00:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -220,7 +220,17 @@ msgstr ""
"auswählen, wenn ein LJP-Antrag abgegeben wird."
msgid "Price per night"
-msgstr "Preis pro Nacht"
+msgstr "Preis pro Übernachtung"
+
+#, python-format
+msgid ""
+"Price for the overnight stay of a youth leader. this is required for the "
+"calculation of the subsidies for night costs. The maximum subsidised value "
+"is %(max_cost)s€."
+msgstr ""
+"Laut Preisliste für einen Jugendleiter/eine Jugendleiterin. Angabe wird "
+"benötigt für die Berechnung von Zuschüssen aus dem Jugendetat. Maximaler "
+"Zuschuss pro Person und Nacht: %(max_cost)s €"
msgid "Status"
msgstr "Status"
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 831a42c6..5c0faed6 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -116,7 +116,14 @@ class Statement(CommonModel):
)
night_cost = models.DecimalField(
- verbose_name=_("Price per night"), default=0, decimal_places=2, max_digits=5
+ verbose_name=_("Price per night"),
+ default=0,
+ decimal_places=2,
+ max_digits=5,
+ help_text=_(
+ "Price for the overnight stay of a youth leader. this is required for the calculation of the subsidies for night costs. The maximum subsidised value is %(max_cost)s€."
+ )
+ % {"max_cost": settings.MAX_NIGHT_COST},
)
status = models.IntegerField(
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index 1e46a6ba..b0d85c9c 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -1787,12 +1787,11 @@ class FreizeitAdmin(ExtraButtonsMixin, CommonAdminMixin, nested_admin.NestedMode
"description",
"groups",
"jugendleiter",
- "approved_extra_youth_leader_count",
+ "activity",
+ "difficulty",
"tour_type",
"tour_approach",
"kilometers_traveled",
- "activity",
- "difficulty",
),
"description": _(
"General information on your excursion. These are partly relevant for the amount of financial compensation (means of transport, travel distance, etc.)."
@@ -1802,7 +1801,7 @@ class FreizeitAdmin(ExtraButtonsMixin, CommonAdminMixin, nested_admin.NestedMode
(
_("Approval"),
{
- "fields": ("approved", "approval_comments"),
+ "fields": ("approved", "approval_comments", "approved_extra_youth_leader_count"),
"description": _(
"Information on the approval status of this excursion. Everything here is not editable by standard users."
),
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index af94ad00..1b8d4293 100644
--- a/jdav_web/members/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/members/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: 2026-01-18 11:05+0100\n"
+"POT-Creation-Date: 2026-02-12 00:02+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -608,6 +608,9 @@ msgstr "Stützpunkt / Ort"
msgid "Postcode"
msgstr "PLZ"
+msgid "only relevant for a LJP application"
+msgstr "nur für einen LJP-Antrag relevant"
+
msgid "Destination (optional)"
msgstr "ggf. Ziel"
@@ -636,6 +639,13 @@ msgstr ""
msgid "Kilometers traveled"
msgstr "Fahrstrecke in Kilometer"
+msgid ""
+"The total kilometers traveled (away and back) during this excursion. This is "
+"relevant for the section subsidies."
+msgstr ""
+"Gesamte Fahrtstrecke (Hin- und Rückfahrt). Die Angabe ist relevant für die "
+"Berechnung der Zuschüsse durch den Jugendetat."
+
msgid "Categories"
msgstr "Kategorien"
@@ -851,10 +861,10 @@ msgstr ""
"eine Begründung an. Sonst lass dieses Feld frei."
msgid "LJP Proposal"
-msgstr "Seminarbericht"
+msgstr "LJP-Antrag"
msgid "LJP Proposals"
-msgstr "Seminarberichte"
+msgstr "LJP-Anträge"
msgid "Duration in hours"
msgstr "Dauer in Stunden"
diff --git a/jdav_web/members/models/excursion.py b/jdav_web/members/models/excursion.py
index 7b372ffd..84d7b3d0 100644
--- a/jdav_web/members/models/excursion.py
+++ b/jdav_web/members/models/excursion.py
@@ -41,7 +41,13 @@ class Freizeit(CommonModel):
name = models.CharField(verbose_name=_("Activity"), default="", max_length=50)
place = models.CharField(verbose_name=_("Place"), default="", max_length=50)
- postcode = models.CharField(verbose_name=_("Postcode"), default="", max_length=30, blank=True)
+ postcode = models.CharField(
+ verbose_name=_("Postcode"),
+ default="",
+ max_length=30,
+ blank=True,
+ help_text=_("only relevant for a LJP application"),
+ )
destination = models.CharField(
verbose_name=_("Destination (optional)"),
default="",
@@ -83,6 +89,9 @@ class Freizeit(CommonModel):
verbose_name=_("Kilometers traveled"),
validators=[MinValueValidator(0)],
default=0,
+ help_text=_(
+ "The total kilometers traveled (away and back) during this excursion. This is relevant for the section subsidies."
+ ),
)
activity = models.ManyToManyField(ActivityCategory, default=None, verbose_name=_("Categories"))
difficulty_choices = [(1, _("easy")), (2, _("medium")), (3, _("hard"))]
From d11ef381955a48660a366d36fa060aa71f9a25d2 Mon Sep 17 00:00:00 2001
From: mariusrklein
Date: Thu, 12 Feb 2026 22:37:04 +0100
Subject: [PATCH 2/6] fix(excursion): enable discrimination between yl
activities and regular ones
this includes to the following features:
- calculation of org fee should be disabled for older participants in a yl activity
- calculation of ljp participants is different for yl acitivitis: there is no age limit.
- as a consequence, paid and requested ljp contributions are considerably higher
the ljp goal qualification can only occur with the category staff training (also the negation of both). This is restricted by a field validation so the discrimination can check for just one of the fields.
---
.../finance/locale/de/LC_MESSAGES/django.po | 16 +++++-----
jdav_web/finance/models.py | 14 ++++++--
.../admin/overview_submitted_statement.html | 2 +-
jdav_web/members/admin.py | 32 +++++++++++++++++++
.../members/locale/de/LC_MESSAGES/django.po | 22 +++++++++++--
jdav_web/members/models/excursion.py | 14 +++++---
.../admin/freizeit_finance_overview.html | 2 +-
7 files changed, 82 insertions(+), 20 deletions(-)
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index 8e26e899..4f7df240 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: 2026-02-12 00:14+0100\n"
+"POT-Creation-Date: 2026-02-12 22:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -484,18 +484,18 @@ msgstr "LJP-Zuschüsse"
msgid ""
" The youth leaders have documented interventions worth of "
"%(total_seminar_days)s seminar\n"
-"days for %(participant_count)s eligible participants. Taking into account "
-"the maximum contribution quota\n"
+"days for %(ljp_participant_count)s eligible participants. Taking into "
+"account the maximum contribution quota\n"
"of 90%% and possible taxes (%(ljp_tax)s%%), this results in a total of "
"%(paid_ljp_contributions)s€.\n"
"Once their proposal was approved, the ljp contributions of should be paid to:"
msgstr ""
"Jugendleiter*innen haben Lerneinheiten für insgesamt %(total_seminar_days)s "
-"Seminartage und für %(participant_count)s Teilnehmende dokumentiert. Unter "
-"Einbezug der maximalen Förderquote von 90%% und möglichen Steuern "
-"(%(ljp_tax)s%%), ergibt sich ein auszuzahlender Betrag von "
-"%(paid_ljp_contributions)s€. Sobald der LJP-Antrag geprüft ist, können LJP-"
-"Zuschüsse ausbezahlt werden an:"
+"Seminartage und für %(ljp_participant_count)s Teilnehmende mit Anspruch auf "
+"LJP-Zuschüsse dokumentiert. Unter Einbezug der maximalen Förderquote von "
+"90%% und möglichen Steuern (%(ljp_tax)s%%), ergibt sich ein auszuzahlender "
+"Betrag von %(paid_ljp_contributions)s€. Sobald der LJP-Antrag geprüft ist, "
+"können LJP-Zuschüsse ausbezahlt werden an:"
msgid "Summary"
msgstr "Zusammenfassung"
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 5c0faed6..29e49268 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -558,9 +558,16 @@ def total_org_fee_theoretical(self):
@property
def total_org_fee(self):
"""only calculate org fee if subsidies or allowances are claimed."""
- if self.subsidy_to or self.allowances_paid > 0:
- return self.total_org_fee_theoretical
- return cvt_to_decimal(0)
+ if not self.subsidy_to and self.allowances_paid == 0:
+ return cvt_to_decimal(0)
+
+ # if the excursion is for qualification, we don't charge org fees for older participants.
+ if hasattr(self.excursion, "ljpproposal"):
+ proposal = getattr(self.excursion, "ljpproposal")
+ if proposal.goal == proposal.LJP_QUALIFICATION:
+ return cvt_to_decimal(0)
+
+ return self.total_org_fee_theoretical
@property
def org_fee_payant(self):
@@ -693,6 +700,7 @@ def template_context(self):
"paid_ljp_contributions": self.paid_ljp_contributions,
"ljp_to": self.ljp_to,
"theoretic_ljp_participant_count": self.excursion.theoretic_ljp_participant_count,
+ "ljp_participant_count": self.excursion.ljp_participant_count,
"participant_count": self.excursion.participant_count,
"total_seminar_days": self.excursion.total_seminar_days,
"ljp_tax": settings.LJP_TAX * 100,
diff --git a/jdav_web/finance/templates/admin/overview_submitted_statement.html b/jdav_web/finance/templates/admin/overview_submitted_statement.html
index 9aacf8b7..43dd296f 100644
--- a/jdav_web/finance/templates/admin/overview_submitted_statement.html
+++ b/jdav_web/finance/templates/admin/overview_submitted_statement.html
@@ -106,7 +106,7 @@ {% trans "Org fee" %}
{% trans "LJP contributions" %}
{% blocktrans %} The youth leaders have documented interventions worth of {{ total_seminar_days }} seminar
-days for {{ participant_count }} eligible participants. Taking into account the maximum contribution quota
+days for {{ ljp_participant_count }} eligible participants. Taking into account the maximum contribution quota
of 90% and possible taxes ({{ ljp_tax }}%), this results in a total of {{ paid_ljp_contributions }}€.
Once their proposal was approved, the ljp contributions of should be paid to:{% endblocktrans %}
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index b0d85c9c..cef77c8f 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -1576,6 +1576,37 @@ class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline):
formfield_overrides = {TextField: {"widget": Textarea(attrs={"rows": 1, "cols": 80})}}
+class LJPProposalForm(forms.ModelForm):
+ """Custom form for the `LJPOnListInline` with validation rules"""
+
+ class Meta:
+ model = LJPProposal
+ exclude = []
+
+ def clean(self):
+ cleaned_data = super().clean()
+ goal = cleaned_data.get("goal")
+ category = cleaned_data.get("category")
+
+ if goal is not None and category is not None:
+ # LJP_QUALIFICATION (goal=1) can only combine with LJP_STAFF_TRAINING (category=1)
+ if goal == LJPProposal.LJP_QUALIFICATION:
+ if category != LJPProposal.LJP_STAFF_TRAINING:
+ raise ValidationError(
+ _(
+ "The learning goal 'Qualification' can only be combined with the category 'Staff training'."
+ )
+ )
+ # All other goals can only combine with LJP_EDUCATIONAL (category=2)
+ else:
+ if category != LJPProposal.LJP_EDUCATIONAL:
+ raise ValidationError(
+ _(
+ "The learning goals 'Participation', 'Personality development', and 'Environment' can only be combined with the category 'Educational programme'."
+ )
+ )
+
+
class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = LJPProposal
extra = 1
@@ -1584,6 +1615,7 @@ class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
)
sortable_options = []
inlines = [InterventionOnLJPInline]
+ form = LJPProposalForm
class MemberOnListInlineForm(forms.ModelForm):
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 1b8d4293..414a3f69 100644
--- a/jdav_web/members/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/members/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: 2026-02-12 00:02+0100\n"
+"POT-Creation-Date: 2026-02-12 22:35+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -407,6 +407,21 @@ msgstr ""
"einzelnen Posten wird dabei auf der LJP-Kostenübersicht angezeigt (sinnvoll "
"wären z.B. Anreise, Verpflegung, Material etc.)."
+msgid ""
+"The learning goal 'Qualification' can only be combined with the category "
+"'Staff training'."
+msgstr ""
+"Das Lernziel 'Qualifizierung' kann nur in Kombination mit der Kategorie "
+"'Jugendleiter*innenweiterbildung' verwendet werden."
+
+msgid ""
+"The learning goals 'Participation', 'Personality development', and "
+"'Environment' can only be combined with the category 'Educational programme'."
+msgstr ""
+"Die Lernziele 'Partizipation', 'Persönlichkeitsentwicklung' und "
+"'Umweltbildung' können nur mit der Kategorie 'Themenorientierte "
+"Bildungsmaßnahme' verwendet werden."
+
msgid ""
"Here you can work on a seminar report for applying for financial "
"contributions from Landesjugendplan (LJP). More information on creating a "
@@ -1316,14 +1331,15 @@ msgstr "LJP Zuschüsse"
msgid ""
"By submitting the given seminar report, you will receive LJP contributions.\n"
"You have documented interventions worth of %(total_seminar_days)s seminar "
-"days for %(participant_count)s participants.\n"
+"days for %(ljp_participant_count)s participants.\n"
"This results in a total contribution of %(ljp_contributions)s€.\n"
"To receive them, you need to submit the LJP-Proposal within 3 weeks after "
"your excursion and have it approved by the finance office."
msgstr ""
"Wenn du den erstellten LJP-Antrag einreichst, erhältst du LJP-Zuschüsse. Du "
"hast Lehreinheiten für insgesamt %(total_seminar_days)s Seminartage und für "
-"%(participant_count)s Teilnehmende dokumentiert.\n"
+"%(ljp_participant_count)s Teilnehmende mit Anspruch auf LJP-Zuschüsse "
+"dokumentiert.\n"
"Daraus ergibt sich ein auszahlbarer LJP-Zuschuss von %(ljp_contributions)s€. "
"Um den zu erhalten, musst du den LJP-Antrag innerhalb von 3 Wochen nach der "
"Ausfahrt beim Jugendreferat einreichen und formal genehmigt bekommen."
diff --git a/jdav_web/members/models/excursion.py b/jdav_web/members/models/excursion.py
index 84d7b3d0..3758b14d 100644
--- a/jdav_web/members/models/excursion.py
+++ b/jdav_web/members/models/excursion.py
@@ -309,10 +309,16 @@ def theoretic_ljp_participant_count(self):
jls = set(self.jugendleiter.distinct())
# non-youth leader participants
ps_only = ps - jls
- # participants of the correct age
- ps_correct_age = {
- m for m in ps_only if m.age_at(self.date) >= 6 and m.age_at(self.date) < 27
- }
+ # participants of the correct age (age does not matter for excursions with goal qualification)
+ if (
+ hasattr(self, "ljpproposal")
+ and self.ljpproposal.goal == self.ljpproposal.LJP_QUALIFICATION
+ ):
+ ps_correct_age = ps_only
+ else:
+ ps_correct_age = {
+ m for m in ps_only if m.age_at(self.date) >= 6 and m.age_at(self.date) < 27
+ }
# m = the official non-youth-leader participant count
# and, assuming there exist enough participants, unrounded m satisfies the equation
# len(ps_correct_age) + 1/5 * m = m
diff --git a/jdav_web/members/templates/admin/freizeit_finance_overview.html b/jdav_web/members/templates/admin/freizeit_finance_overview.html
index 8886d33b..cc321d1e 100644
--- a/jdav_web/members/templates/admin/freizeit_finance_overview.html
+++ b/jdav_web/members/templates/admin/freizeit_finance_overview.html
@@ -122,7 +122,7 @@ {% trans "LJP contributions" %}
{% blocktrans %}By submitting the given seminar report, you will receive LJP contributions.
-You have documented interventions worth of {{ total_seminar_days }} seminar days for {{ participant_count }} participants.
+You have documented interventions worth of {{ total_seminar_days }} seminar days for {{ ljp_participant_count }} participants.
This results in a total contribution of {{ ljp_contributions }}€.
To receive them, you need to submit the LJP-Proposal within 3 weeks after your excursion and have it approved by the finance office.{% endblocktrans %}
From 804e53efe27cce05bc8ef58af6bae85740649726 Mon Sep 17 00:00:00 2001
From: mariusrklein
Date: Thu, 12 Feb 2026 23:52:49 +0100
Subject: [PATCH 3/6] feat(excursion): add full receipt overview for ljp
proposal
the document
- lists all bills for an activity
- adds up all allowances on top of the bill overview
- adds a custom receipt for the allowances
- appends all other receipts in their order on the list
---
.../templates/finance/ljp_statement.tex | 108 ++++++++++++++++++
jdav_web/members/admin.py | 38 ++++++
.../members/locale/de/LC_MESSAGES/django.po | 18 ++-
.../admin/generate_seminar_report.html | 12 ++
4 files changed, 173 insertions(+), 3 deletions(-)
create mode 100644 jdav_web/finance/templates/finance/ljp_statement.tex
diff --git a/jdav_web/finance/templates/finance/ljp_statement.tex b/jdav_web/finance/templates/finance/ljp_statement.tex
new file mode 100644
index 00000000..924b2a20
--- /dev/null
+++ b/jdav_web/finance/templates/finance/ljp_statement.tex
@@ -0,0 +1,108 @@
+{% extends "members/tex_base.tex" %}
+{% load static common tex_extras %}
+
+{% block title %}Belegübersicht für LJP-Verwendungsnachweis{% endblock %}
+
+{% block content %}
+
+{% if excursion %}
+
+% 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.time_period_str|esc_all }} \\
+ \end{tabular}
+\end{table}
+
+\vspace{12pt}
+
+\noindent\textbf{\large Belegübersicht}
+
+\noindent Die folgenden Ausgaben wurden im Zusammenhang mit der oben genannte Aktivität getätigt:
+
+\begin{table}[H]
+ \centering
+ \begin{tabularx}{.97\textwidth}{lXlr}
+ \toprule
+ \textbf{Nr.} & \textbf{Beschreibung} & \textbf{Erklärung} & \textbf{Betrag} \\
+ \midrule
+
+{% if all_bills or total_allowance > 0 %}
+ {% if total_allowance > 0 %}
+ 1. & Aufwandsentschädigungen & Gezahlte Aufwandsentschädigungen für Jugendleiter*innen & {{ total_allowance }} € \\
+ {% endif %}
+ {% if all_bills %}
+ {% for bill in all_bills %}
+ {% if total_allowance > 0 %}
+ {{ forloop.counter|plus:1 }}. & {{ bill.short_description|esc_all }} & {{ bill.explanation|esc_all }} & {{ bill.amount }} € \\
+ {% else %}
+ {{ forloop.counter }}. & {{ bill.short_description|esc_all }} & {{ bill.explanation|esc_all }} & {{ bill.amount }} € \\
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ \midrule
+ \multicolumn{3}{l}{\textbf{Gesamtsumme}} & \textbf{ {{ total_theoretic }} }€\\
+{% else %}
+ \multicolumn{4}{l}{\textit{Keine Rechnungen für LJP-Abrechnung vorhanden.}} \\
+{% endif %}
+ \bottomrule
+ \end{tabularx}
+\end{table}
+
+{% if all_bills %}
+
+\noindent\textbf{\large Anhänge - Belege}
+
+\noindent Die folgenden Seiten enthalten die Kopien der oben aufgeführten Rechnungen und Belege.
+
+{% endif %}
+
+{% if total_allowance > 0 %}
+
+\newpage
+
+\noindent\textbf{\large Quittung Aufwandsentschädigungen}
+
+
+% DESCRIPTION TABLE
+\begin{table}[H]
+ \begin{tabular}{ll}
+ Aktivität: & {{ excursion.name|esc_all }} \\
+ Ordnungsnummer & {{ excursion.code|esc_all }} \\
+ Zeitraum: & {{ excursion.time_period_str|esc_all }} \\
+ \end{tabular}
+\end{table}
+
+\vspace{12pt}
+
+\noindent\textbf{\large Aufwandsentschädigungen}
+
+\noindent Die folgenden Jugendleiter*innen erhalten eine Aufwandsentschädigung. Insgesamt werden {{ total_allowance }} € ausbezahlt.
+
+\begin{table}[H]
+ \centering
+ \begin{tabularx}{.97\textwidth}{lXlr}
+ \toprule
+ \textbf{Nr.} & \textbf{Name} & \textbf{Betrag pro Person} & \textbf{Gesamtbetrag} \\
+ \midrule
+ {% for allowance_member in allowance_to %}
+ {{ forloop.counter }}. & {{ allowance_member.name|esc_all }} & {{ allowance_per_yl }} € & {{ allowance_per_yl }} € \\
+ {% endfor %}
+ \midrule
+ \multicolumn{3}{l}{\textbf{Gesamtsumme Aufwandsentschädigungen}} & \textbf{ {{ total_allowance }} }€\\
+ \bottomrule
+ \end{tabularx}
+\end{table}
+
+\noindent Dieser Beleg dokumentiert die gezahlten Aufwandsentschädigungen und wird automatisch erstellt.
+
+{% endif %}
+
+{% else %}
+\vspace{110pt}
+{% endif %}
+
+{% endblock %}
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index cef77c8f..1814c09e 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -35,6 +35,7 @@
from finance.models import BillOnExcursionProxy
from finance.models import StatementOnExcursionProxy
from mailer.models import Message
+from members.pdf import render_tex_with_attachments
from schwifty import IBAN
from utils import get_member
from utils import mondays_until_nth
@@ -1962,6 +1963,38 @@ def download_seminar_report_costs_and_participants(self, request, memberlist):
date=memberlist.date,
)
+ @decorate_download
+ def download_ljp_proofs(self, request, memberlist):
+ if not hasattr(memberlist, "statement"):
+ messages.error(request, _("This excursion does not have a statement."))
+ return HttpResponseRedirect(
+ reverse(
+ "admin:{}_{}_change".format(self.opts.app_label, self.opts.model_name),
+ args=(memberlist.pk,),
+ )
+ )
+
+ statement = memberlist.statement
+ all_bills = list(statement.bill_set.all())
+
+ context = dict(
+ statement=statement,
+ excursion=memberlist,
+ all_bills=all_bills,
+ total_bills=statement.total_bills_theoretic,
+ total_allowance=statement.total_allowance,
+ total_theoretic=statement.total_theoretic,
+ allowance_to=statement.allowance_to.all(),
+ allowance_per_yl=statement.allowance_per_yl,
+ settings=settings,
+ )
+
+ pdf_filename = f"{memberlist.code}_{memberlist.name}_LJP_Nachweis"
+ attachments = [bill.proof.path for bill in all_bills if bill.proof]
+ return render_tex_with_attachments(
+ pdf_filename, "finance/ljp_statement.tex", context, attachments
+ )
+
@extra_button(
_("Generate seminar report"), method="POST", permission=may_view_excursion.__func__
)
@@ -2144,6 +2177,11 @@ def wrapper(*args, **kwargs):
self.opts.app_label, self.opts.model_name
),
),
+ path(
+ "/download/ljp_proofs",
+ wrap(self.download_ljp_proofs),
+ name="{}_{}_download_ljp_proofs".format(self.opts.app_label, self.opts.model_name),
+ ),
]
return custom_urls + urls
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 414a3f69..053a69a5 100644
--- a/jdav_web/members/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/members/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: 2026-02-12 22:35+0100\n"
+"POT-Creation-Date: 2026-02-12 23:41+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -493,6 +493,9 @@ msgstr "Kriseninterventionsliste erstellen"
msgid "Generate overview"
msgstr "Hinweise für Jugendleiter erstellen"
+msgid "This excursion does not have a statement."
+msgstr "Diese Ausfahrt hat noch keine Abrechnung."
+
msgid "Generate seminar report"
msgstr "Landesjugendplan Antrag erstellen"
@@ -1493,9 +1496,18 @@ msgstr ""
msgid ""
"A cost and participants overview. This is not required for the actual "
"application, but is provided for convience as a PDF document."
+msgstr "Eine Kosten- und Teilnehmendenübersicht."
+
+msgid ""
+"A collection of all invoices and receipts as evidence for the LJP "
+"application, including a summary table and all attached proofs."
msgstr ""
-"Eine Kosten- und Teilnehmendenübersicht. Dies ist nicht notwendig für den "
-"eigentlichen Bericht, muss aber langfristig aufbewahrt werden."
+"Eine Belegübersicht mit Nachweisen über alle Ausgaben. Dies ist nicht "
+"notwendig für den eigentlichen Antrag, muss aber langfristig aufbewahrt "
+"werden."
+
+msgid "No statement available"
+msgstr "Keine Ausgaben verfügbar"
msgid "Here you can generate an allowance application for the SJR."
msgstr "Hier kannst du einen SJR-Zuschussantrag erstellen."
diff --git a/jdav_web/members/templates/admin/generate_seminar_report.html b/jdav_web/members/templates/admin/generate_seminar_report.html
index 46331088..d76c32a8 100644
--- a/jdav_web/members/templates/admin/generate_seminar_report.html
+++ b/jdav_web/members/templates/admin/generate_seminar_report.html
@@ -39,6 +39,18 @@ {% trans 'LJP application for' %}: {{ memberlist.ljpproposal.title }} ({{ me
{% translate "Download" %}
+
+|
+{% blocktrans %}A collection of all invoices and receipts as evidence for the LJP application, including a summary table and all attached proofs.{% endblocktrans %}
+ |
+
+{% if memberlist.statement %}
+{% translate "Download" %}
+{% else %}
+{% trans 'No statement available' %}
+{% endif %}
+ |
+
From dcc2e8ffb7835853d558989606a6ade6ed36732a Mon Sep 17 00:00:00 2001
From: marius <47218379+mariusrklein@users.noreply.github.com>
Date: Sun, 8 Mar 2026 21:58:11 +0100
Subject: [PATCH 4/6] Update jdav_web/members/locale/de/LC_MESSAGES/django.po
Co-authored-by: Christian Merten
---
jdav_web/members/locale/de/LC_MESSAGES/django.po | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 053a69a5..31d6192e 100644
--- a/jdav_web/members/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/members/locale/de/LC_MESSAGES/django.po
@@ -418,7 +418,7 @@ msgid ""
"The learning goals 'Participation', 'Personality development', and "
"'Environment' can only be combined with the category 'Educational programme'."
msgstr ""
-"Die Lernziele 'Partizipation', 'Persönlichkeitsentwicklung' und "
+"Die Bildungsziele 'Partizipation', 'Persönlichkeitsentwicklung' und "
"'Umweltbildung' können nur mit der Kategorie 'Themenorientierte "
"Bildungsmaßnahme' verwendet werden."
From 029d11a7d4f841d3a2147ddbeafdc67bf8d66543 Mon Sep 17 00:00:00 2001
From: marius <47218379+mariusrklein@users.noreply.github.com>
Date: Sun, 8 Mar 2026 21:58:23 +0100
Subject: [PATCH 5/6] Update jdav_web/finance/locale/de/LC_MESSAGES/django.po
Co-authored-by: Christian Merten
---
jdav_web/finance/locale/de/LC_MESSAGES/django.po | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index 4f7df240..412110cd 100644
--- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
@@ -228,7 +228,7 @@ msgid ""
"calculation of the subsidies for night costs. The maximum subsidised value "
"is %(max_cost)s€."
msgstr ""
-"Laut Preisliste für einen Jugendleiter/eine Jugendleiterin. Angabe wird "
+"Laut Preisliste für eine*n Jugendleiter*in. Angabe wird "
"benötigt für die Berechnung von Zuschüssen aus dem Jugendetat. Maximaler "
"Zuschuss pro Person und Nacht: %(max_cost)s €"
From b87d613553ac430f1df7d504b7b2380318333c37 Mon Sep 17 00:00:00 2001
From: marius <47218379+mariusrklein@users.noreply.github.com>
Date: Sun, 8 Mar 2026 21:59:09 +0100
Subject: [PATCH 6/6] Update jdav_web/members/admin.py
Co-authored-by: Christian Merten
---
jdav_web/members/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index 1814c09e..a16f41e0 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -1590,7 +1590,7 @@ def clean(self):
category = cleaned_data.get("category")
if goal is not None and category is not None:
- # LJP_QUALIFICATION (goal=1) can only combine with LJP_STAFF_TRAINING (category=1)
+ # LJP_QUALIFICATION can only combine with LJP_STAFF_TRAINING
if goal == LJPProposal.LJP_QUALIFICATION:
if category != LJPProposal.LJP_STAFF_TRAINING:
raise ValidationError(