Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions jdav_web/finance/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 22:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -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 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 €"

msgid "Status"
msgstr "Status"
Expand Down Expand Up @@ -474,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"
Expand Down
23 changes: 19 additions & 4 deletions jdav_web/finance/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -551,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):
Expand Down Expand Up @@ -686,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ <h3>{% trans "Org fee" %}</h3>
<h3>{% trans "LJP contributions" %}</h3>
<p>
{% 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 %}
<table>
Expand Down
108 changes: 108 additions & 0 deletions jdav_web/finance/templates/finance/ljp_statement.tex
Original file line number Diff line number Diff line change
@@ -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 %}
77 changes: 73 additions & 4 deletions jdav_web/members/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1576,6 +1577,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 can only combine with LJP_STAFF_TRAINING
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
Expand All @@ -1584,6 +1616,7 @@ class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
)
sortable_options = []
inlines = [InterventionOnLJPInline]
form = LJPProposalForm


class MemberOnListInlineForm(forms.ModelForm):
Expand Down Expand Up @@ -1787,12 +1820,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.)."
Expand All @@ -1802,7 +1834,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."
),
Expand Down Expand Up @@ -1931,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__
)
Expand Down Expand Up @@ -2113,6 +2177,11 @@ def wrapper(*args, **kwargs):
self.opts.app_label, self.opts.model_name
),
),
path(
"<path:object_id>/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

Expand Down
Loading
Loading