From 3250dc7089ab31c75628da38a8b6b94d23d98509 Mon Sep 17 00:00:00 2001
From: "marius.klein"
Date: Mon, 28 Apr 2025 09:19:02 +0200
Subject: [PATCH] feat(finance): org fee for old participants (#149)
Allow deducting a configurable organisational fee for participants older than 27 from subsidies. This is calculated per day and old participant.
Reviewed-on: https://git.jdav-hd.merten.dev/digitales/kompass/pulls/149
Reviewed-by: Christian Merten
Co-authored-by: marius.klein
Co-committed-by: marius.klein
---
.../finance/locale/de/LC_MESSAGES/django.po | 27 +++++++++-
jdav_web/finance/models.py | 52 +++++++++++++++++--
.../admin/overview_submitted_statement.html | 17 ++++++
.../templates/finance/statement_summary.tex | 11 +++-
jdav_web/jdav_web/settings/local.py | 2 +
.../members/locale/de/LC_MESSAGES/django.po | 32 ++++++++++--
jdav_web/members/models.py | 13 ++++-
.../admin/freizeit_finance_overview.html | 19 ++++++-
8 files changed, 158 insertions(+), 15 deletions(-)
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index 5cadb5a..df726c6 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-04-06 18:46+0200\n"
+"POT-Creation-Date: 2025-04-27 23:00+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -297,6 +297,10 @@ msgstr "Aufwandsentschädigung für %(excu)s"
msgid "Night and travel costs for %(excu)s"
msgstr "Übernachtungs- und Fahrtkosten für %(excu)s"
+#: finance/models.py
+msgid "reduced by org fee"
+msgstr "reduziert um Org-Beitrag"
+
#: finance/models.py
#, python-format
msgid "LJP-Contribution %(excu)s"
@@ -541,6 +545,27 @@ msgstr ""
"Keine Empfänger*innen für Sektionszuschüsse angegeben. Es werden daher keine "
"Sektionszuschüsse ausbezahlt."
+#: finance/templates/admin/overview_submitted_statement.html
+msgid "Org fee"
+msgstr "Organisationsbeitrag"
+
+#: finance/templates/admin/overview_submitted_statement.html
+#, python-format
+msgid ""
+"Since overaged people where part of the excursion, an organisational fee of "
+"%(org_fee)s€ per person per day has to be paid. This totals to "
+"%(total_org_fee_theoretical)s€. This organisational fee will be accounted "
+"against allowances and subsidies."
+msgstr ""
+"Da Personen über 27 an der Ausfahrt teilnehommen haben, wird ein "
+"Organisationsbeitrag von %(org_fee)s€ pro Person und Tag fällig. Der Gesamtbetrag "
+"von %(total_org_fee_theoretical)s€ wird mit Zuschüssen und "
+"Aufwandsentschädigungen verrechnet."
+
+#: finance/templates/admin/overview_submitted_statement.html
+msgid "LJP contributions"
+msgstr "LJP-Zuschüsse"
+
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
msgid ""
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 92a4c08..2e4b3e1 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -135,6 +135,11 @@ class Statement(CommonModel):
needed_paiments.extend([(yl, self.allowance_per_yl) for yl in self.allowance_to.all()])
if self.subsidy_to:
needed_paiments.append((self.subsidy_to, self.total_subsidies))
+
+ # only include org fee if either allowance or subsidy is claimed (part of the property)
+ if self.total_org_fee:
+ needed_paiments.append((self.org_fee_payant, -self.total_org_fee))
+
if self.ljp_to:
needed_paiments.append((self.ljp_to, self.paid_ljp_contributions))
@@ -250,11 +255,17 @@ class Statement(CommonModel):
if self.subsidy_to:
ref = _("Night and travel costs for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=self.subsidy_to, amount=self.total_subsidies, confirmed=False, reference=ref).save()
-
+
+ if self.total_org_fee:
+ # if no subsidy receiver is given but org fees have to be paid. Just pick one of allowance receivers
+ ref = _("reduced by org fee")
+ Transaction(statement=self, member=self.org_fee_payant, amount=-self.total_org_fee, confirmed=False, reference=ref).save()
+
if self.ljp_to:
ref = _("LJP-Contribution %(excu)s") % {'excu': self.excursion.name}
- Transaction(statement=self, member=self.ljp_to, amount=self.paid_ljp_contributions, confirmed=False, reference=ref).save()
-
+ Transaction(statement=self, member=self.ljp_to, amount=self.paid_ljp_contributions,
+ confirmed=False, reference=ref).save()
+
return True
def reduce_transactions(self):
@@ -368,6 +379,24 @@ class Statement(CommonModel):
return cvt_to_decimal(self.total_staff / self.excursion.staff_count)
+ @property
+ def total_org_fee_theoretical(self):
+ """participants older than 26.99 years need to pay a specified organisation fee per person per day."""
+ if self.excursion is None:
+ return 0
+ return cvt_to_decimal(settings.EXCURSION_ORG_FEE * self.excursion.duration * self.excursion.old_participant_count)
+
+ @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)
+
+ @property
+ def org_fee_payant(self):
+ return self.subsidy_to if self.subsidy_to else self.allowance_to.all()[0]
+
@property
def total_subsidies(self):
"""
@@ -379,6 +408,10 @@ class Statement(CommonModel):
else:
return cvt_to_decimal(0)
+ @property
+ def subsidies_paid(self):
+ return self.total_subsidies - self.total_org_fee
+
@property
def theoretical_total_staff(self):
"""
@@ -393,6 +426,11 @@ class Statement(CommonModel):
"""
return self.total_allowance + self.total_subsidies
+ @property
+ def total_staff_paid(self):
+ return self.total_staff - self.total_org_fee
+
+
@property
def real_staff_count(self):
if self.excursion is None:
@@ -428,7 +466,7 @@ class Statement(CommonModel):
@property
def total(self):
- return self.total_bills + self.total_staff + self.paid_ljp_contributions
+ return self.total_bills + self.total_staff_paid + self.paid_ljp_contributions
@property
def total_theoretic(self):
@@ -464,6 +502,7 @@ class Statement(CommonModel):
'allowances_paid': self.allowances_paid,
'nights_per_yl': self.nights_per_yl,
'allowance_per_yl': self.allowance_per_yl,
+ 'total_allowance': self.total_allowance,
'transportation_per_yl': self.transportation_per_yl,
'total_per_yl': self.total_per_yl,
'total_staff': self.total_staff,
@@ -480,6 +519,11 @@ class Statement(CommonModel):
'participant_count': self.excursion.participant_count,
'total_seminar_days': self.excursion.total_seminar_days,
'ljp_tax': settings.LJP_TAX * 100,
+ 'total_org_fee_theoretical': self.total_org_fee_theoretical,
+ 'total_org_fee': self.total_org_fee,
+ 'old_participant_count': self.excursion.old_participant_count,
+ 'total_staff_paid': self.total_staff_paid,
+ 'org_fee': cvt_to_decimal(settings.EXCURSION_ORG_FEE),
}
return dict(context, **excursion_context)
else:
diff --git a/jdav_web/finance/templates/admin/overview_submitted_statement.html b/jdav_web/finance/templates/admin/overview_submitted_statement.html
index 0d97736..c8dc382 100644
--- a/jdav_web/finance/templates/admin/overview_submitted_statement.html
+++ b/jdav_web/finance/templates/admin/overview_submitted_statement.html
@@ -109,11 +109,20 @@
+
{% else %}
{% blocktrans %}No receivers of the subsidies were provided. Subsidies will not be used.{% endblocktrans %}
{% endif %}
+{% if total_org_fee %}
+{% trans "Org fee" %}
+{% blocktrans %}Since overaged people where part of the excursion, an organisational fee of {{ org_fee }}€ per person per day has to be paid. This totals to {{ total_org_fee_theoretical }}€. This organisational fee will be accounted against allowances and subsidies.{% endblocktrans %}
+
+{% endif %}
+
+
{% if statement.ljp_to %}
+{% 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
@@ -163,6 +172,14 @@ Once their proposal was approved, the ljp contributions of should be paid to:{%
{{ total_subsidies }}€
+
+ |
+ {% trans "Org fee" %}
+ |
+
+ -{{ total_org_fee }}€
+ |
+
|
{% trans "ljp contributions" %}
diff --git a/jdav_web/finance/templates/finance/statement_summary.tex b/jdav_web/finance/templates/finance/statement_summary.tex
index ee37f02..99657e4 100644
--- a/jdav_web/finance/templates/finance/statement_summary.tex
+++ b/jdav_web/finance/templates/finance/statement_summary.tex
@@ -83,6 +83,12 @@ in der Ausgabenübersicht gesondert aufgeführt.
{% endif %}
+{% if statement.total_org_fee %}
+\noindent\textbf{Organisationsbeitrag}
+
+\noindent An der Ausfahrt haben {{ statement.old_participant_count }} Personen teilgenommen, die 27 Jahre alt oder älter sind. Für sie wird pro Tag ein Organisationsbeitrag von {{ statement.org_fee }} € erhoben und mit den bezahlten Zuschüssen und Aufwandsentschädigungen verrechnet.
+{% endif %}
+
{% else %}
\vspace{110pt}
{% endif %}
@@ -116,8 +122,11 @@ in der Ausgabenübersicht gesondert aufgeführt.
{% if statement.subsidy_to %}
\multicolumn{2}{l}{Zuschuss Übernachtung und Anreise für alle Jugendleiter*innen} & {{ statement.subsidy_to.name|esc_all }} & {{ statement.total_subsidies }} €\\
{% endif %}
+ {% if statement.total_org_fee %}
+ \multicolumn{2}{l}{abzüglich Organisationsbeitrag für {{ statement.old_participant_count }} Teilnehmende über 27 } & & -{{ statement.total_org_fee }} €\\
+ {% endif %}
\midrule
- \multicolumn{3}{l}{\textbf{Summe Zuschüsse und Aufwandsentschädigung Jugendleitende}} & \textbf{ {{ statement.total_staff }} }€\\
+ \multicolumn{3}{l}{\textbf{Summe Zuschüsse und Aufwandsentschädigung Jugendleitende}} & \textbf{ {{ statement.total_staff_paid }} }€\\
{%endif %}
{% if statement.ljp_to %}
\midrule
diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py
index 7d7ebcc..34dbe5c 100644
--- a/jdav_web/jdav_web/settings/local.py
+++ b/jdav_web/jdav_web/settings/local.py
@@ -55,6 +55,8 @@ DOMAIN = get_var('misc', 'domain', default='example.org')
ALLOWANCE_PER_DAY = get_var('finance', 'allowance_per_day', default=22)
MAX_NIGHT_COST = get_var('finance', 'max_night_cost', default=11)
+EXCURSION_ORG_FEE = get_var('finance', 'org_fee', default=10)
+
# links
CLOUD_LINK = get_var('links', 'cloud', default='https://startpage.com')
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 4d6f6dd..3a5cb55 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: 2025-04-15 22:36+0200\n"
+"POT-Creation-Date: 2025-04-27 23:00+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -1379,6 +1379,24 @@ msgstr ""
"Keine Empfänger*innen für Sektionszuschüsse angegeben. Es werden daher keine "
"Sektionszuschüsse ausbezahlt."
+#: members/templates/admin/freizeit_finance_overview.html
+msgid "Org fee"
+msgstr "Organisationsbeitrag"
+
+#: members/templates/admin/freizeit_finance_overview.html
+#, python-format
+msgid ""
+"Warning: %(old_participant_count)s participant(s) of the excursion are 27 or "
+"older. For each of them, an organisation fee of %(org_fee)s € per day has to "
+"be paid to the account. With a duration of %(duration)s days, a total of "
+"%(total_org_fee_theoretical)s € is charged against the other transactions."
+msgstr ""
+"Achtung: %(old_participant_count)s Teilnehmende der Ausfahrt sind 27 oder "
+"älter. Für diese Teilnehmende(n) ist ein Org-Beitrag von %(org_fee)s € pro Tag "
+"fällig. Durch die Länge der Ausfahrt von %(duration)s Tagen werden insgesamt "
+"%(total_org_fee_theoretical)s € mit den Zuschüssen und "
+"Aufwandsentschädigungen verrechnet, sofern diese in Anspruch genommen werden."
+
#: members/templates/admin/freizeit_finance_overview.html
msgid ""
"Warning: The configured recipients of the allowance don't match the "
@@ -1440,10 +1458,10 @@ msgid ""
msgstr ""
"Indem du einen Seminarbericht anfertigst, kannst du Landesjugendplan (LJP) "
"Zuschüsse beantragen. In diesem Fall kannst du bis zu 25€ mal %(duration)s "
-"Tage für %(theoretic_ljp_participant_count)s Teilnehmende, aber nicht mehr als 90%% der "
-"Gesamtausgaben erhalten. Das resultiert in einem Gesamtzuschuss von "
-"%(ljp_contributions)s€. Wenn du schon einen Seminarbericht erstellt hast, "
-"musst du im Tab 'Abrechnungen' noch angeben, an wen die LJP-Zuschüsse "
+"Tage für %(theoretic_ljp_participant_count)s Teilnehmende, aber nicht mehr "
+"als 90%% der Gesamtausgaben erhalten. Das resultiert in einem Gesamtzuschuss "
+"von %(ljp_contributions)s€. Wenn du schon einen Seminarbericht erstellt "
+"hast, musst du im Tab 'Abrechnungen' noch angeben, an wen die LJP-Zuschüsse "
"ausgezahlt werden sollen."
#: members/templates/admin/freizeit_finance_overview.html
@@ -1465,6 +1483,10 @@ msgstr "Zusammenfassung"
msgid "This is the estimated cost and contribution summary:"
msgstr "Das ist die geschätzte Kosten- und Zuschussübersicht."
+#: members/templates/admin/freizeit_finance_overview.html
+msgid "Organisation fees"
+msgstr "Org-Beitrag"
+
#: members/templates/admin/freizeit_finance_overview.html
msgid "Potential LJP contributions"
msgstr "Mögliche LJP Zuschüsse"
diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py
index 16ec653..2542b46 100644
--- a/jdav_web/members/models.py
+++ b/jdav_web/members/models.py
@@ -1344,10 +1344,19 @@ class Freizeit(CommonModel):
@property
def participant_count(self):
+ return len(self.participants)
+
+ @property
+ def participants(self):
ps = set(map(lambda x: x.member, self.membersonlist.distinct()))
jls = set(self.jugendleiter.distinct())
- return len(ps - jls)
-
+ return list(ps - jls)
+
+ @property
+ def old_participant_count(self):
+ old_ps = [m for m in self.participants if m.age() >= 27]
+ return len(old_ps)
+
@property
def head_count(self):
return self.staff_on_memberlist_count + self.participant_count
diff --git a/jdav_web/members/templates/admin/freizeit_finance_overview.html b/jdav_web/members/templates/admin/freizeit_finance_overview.html
index 31c98e6..8cc7a52 100644
--- a/jdav_web/members/templates/admin/freizeit_finance_overview.html
+++ b/jdav_web/members/templates/admin/freizeit_finance_overview.html
@@ -122,17 +122,24 @@ cost plan!
{% else %}
{% blocktrans %}No receivers of the subsidies were provided. Subsidies will not be used.{% endblocktrans %}
+{% endif %}
+{% if total_org_fee %}
+{% trans "Org fee" %}
+
+{% blocktrans %}Warning: {{ old_participant_count }} participant(s) of the excursion are 27 or older. For each of them, an organisation fee of {{ org_fee }} € per day has to be paid to the account. With a duration of {{ duration }} days, a total of {{ total_org_fee_theoretical }} € is charged against the other transactions.{% endblocktrans %}
+
{% endif %}
+
{% if not memberlist.statement.allowance_to_valid %}
{% blocktrans %}Warning: The configured recipients of the allowance don't match the regulations. This might be because the number of recipients is bigger then the number of admissable youth leaders for this excursion.{% endblocktrans %}
{% endif %}
-
-{% if memberlist.statement.ljp_to %}
{% trans "LJP contributions" %}
+{% if memberlist.statement.ljp_to %}
+
{% blocktrans %}By submitting the given seminar report, you will receive LJP contributions.
@@ -204,6 +211,14 @@ you may obtain up to 25€ times {{ duration }} days for {{ theoretic_ljp_partic
{{ total_bills_theoretic }}€
|
+
+ |
+ {% trans "Organisation fees" %}
+ |
+
+ {{ total_org_fee }}€
+ |
+
|
{% trans "Contributions by the association" %}
|