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(