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" %}