feat(finance/excursion): added ljp payout functionality and tax

MK/finance_workflow
mariusrklein 8 months ago
parent 64b7788887
commit c6659e0032

@ -100,7 +100,7 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
memberlist=memberlist, memberlist=memberlist,
object=memberlist, object=memberlist,
participant_count=memberlist.participant_count, participant_count=memberlist.participant_count,
ljp_contributions=memberlist.potential_ljp_contributions, ljp_contributions=memberlist.payable_ljp_contributions,
total_relative_costs=memberlist.total_relative_costs, total_relative_costs=memberlist.total_relative_costs,
**memberlist.statement.template_context()) **memberlist.statement.template_context())
return render(request, 'admin/freizeit_finance_overview.html', context=context) return render(request, 'admin/freizeit_finance_overview.html', context=context)
@ -269,6 +269,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
title=_('View submitted statement'), title=_('View submitted statement'),
opts=self.opts, opts=self.opts,
statement=statement, statement=statement,
settings=settings,
transaction_issues=statement.transaction_issues, transaction_issues=statement.transaction_issues,
**statement.template_context()) **statement.template_context())

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-27 23:35+0100\n" "POT-Creation-Date: 2025-04-04 01:07+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -207,6 +207,16 @@ msgstr ""
"Die Person, die die Übernachtungs- und Fahrtkostenzuschüsse erhalten soll. " "Die Person, die die Übernachtungs- und Fahrtkostenzuschüsse erhalten soll. "
"Dies ist in der Regel die Person, die sie bezahlt hat." "Dies ist in der Regel die Person, die sie bezahlt hat."
#: finance/models.py
msgid "Pay ljp contributions to"
msgstr "LJP-Zuschüsse auszahlen an"
#: finance/models.py
msgid ""
"The person that should receive the ljp contributions for the participants. "
"Should be only selected if an ljp request was submitted."
msgstr "Die Person, die die LJP-Zuschüsse für die Teilnehmenden erhalten soll. Nur auswählen, wenn ein LJP-Antrag abgegeben wird."
#: finance/models.py #: finance/models.py
msgid "Price per night" msgid "Price per night"
msgstr "Preis pro Nacht" msgstr "Preis pro Nacht"
@ -266,6 +276,11 @@ msgstr "Aufwandsentschädigung für %(excu)s"
msgid "Night and travel costs for %(excu)s" msgid "Night and travel costs for %(excu)s"
msgstr "Übernachtungs- und Fahrtkosten für %(excu)s" msgstr "Übernachtungs- und Fahrtkosten für %(excu)s"
#: finance/models.py
#, python-format
msgid "LJP-Contribution %(excu)s"
msgstr "LJP-Zuschuss %(excu)s"
#: finance/models.py finance/templates/admin/overview_submitted_statement.html #: finance/models.py finance/templates/admin/overview_submitted_statement.html
msgid "Total" msgid "Total"
msgstr "Gesamtbetrag" msgstr "Gesamtbetrag"
@ -505,6 +520,22 @@ msgstr ""
"Keine Empfänger*innen für Sektionszuschüsse angegeben. Es werden daher keine " "Keine Empfänger*innen für Sektionszuschüsse angegeben. Es werden daher keine "
"Sektionszuschüsse ausbezahlt." "Sektionszuschüsse ausbezahlt."
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
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"
"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:"
#: finance/templates/admin/overview_submitted_statement.html #: finance/templates/admin/overview_submitted_statement.html
#, python-format #, python-format
msgid "This results in a total amount of %(total)s€" msgid "This results in a total amount of %(total)s€"

@ -0,0 +1,20 @@
# Generated by Django 4.2.20 on 2025-04-03 21:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('members', '0039_membertraining_certificate_attendance'),
('finance', '0008_alter_statement_allowance_to_and_more'),
]
operations = [
migrations.AddField(
model_name='statement',
name='ljp_to',
field=models.ForeignKey(blank=True, help_text='The person that should receive the ljp contributions for the participants. Should be only selected if an ljp request was submitted.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='receives_ljp_for_statements', to='members.member', verbose_name='Pay ljp contributions to'),
),
]

@ -70,6 +70,13 @@ class Statement(CommonModel):
related_name='receives_subsidy_for_statements', related_name='receives_subsidy_for_statements',
help_text=_('The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.')) help_text=_('The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.'))
ljp_to = models.ForeignKey(Member, verbose_name=_('Pay ljp contributions to'),
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='receives_ljp_for_statements',
help_text=_('The person that should receive the ljp contributions for the participants. Should be only selected if an ljp request was submitted.'))
night_cost = models.DecimalField(verbose_name=_('Price per night'), default=0, decimal_places=2, max_digits=5) night_cost = models.DecimalField(verbose_name=_('Price per night'), default=0, decimal_places=2, max_digits=5)
submitted = models.BooleanField(verbose_name=_('Submitted'), default=False) submitted = models.BooleanField(verbose_name=_('Submitted'), default=False)
@ -128,6 +135,8 @@ class Statement(CommonModel):
needed_paiments.extend([(yl, self.allowance_per_yl) for yl in self.allowance_to.all()]) needed_paiments.extend([(yl, self.allowance_per_yl) for yl in self.allowance_to.all()])
if self.subsidy_to: if self.subsidy_to:
needed_paiments.append((self.subsidy_to, self.total_subsidies)) needed_paiments.append((self.subsidy_to, self.total_subsidies))
if self.ljp_to:
needed_paiments.append((self.ljp_to, self.paid_ljp_contributions))
needed_paiments = sorted(needed_paiments, key=lambda p: p[0].pk) needed_paiments = sorted(needed_paiments, key=lambda p: p[0].pk)
target = dict(map(lambda p: (p[0], sum([x[1] for x in p[1]])), groupby(needed_paiments, lambda p: p[0]))) target = dict(map(lambda p: (p[0], sum([x[1] for x in p[1]])), groupby(needed_paiments, lambda p: p[0])))
@ -242,6 +251,10 @@ class Statement(CommonModel):
ref = _("Night and travel costs for %(excu)s") % {'excu': self.excursion.name} 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() Transaction(statement=self, member=self.subsidy_to, amount=self.total_subsidies, 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()
return True return True
def reduce_transactions(self): def reduce_transactions(self):
@ -385,10 +398,18 @@ class Statement(CommonModel):
return 0 return 0
else: else:
return self.excursion.approved_staff_count return self.excursion.approved_staff_count
@property
def paid_ljp_contributions(self):
if hasattr(self.excursion, 'ljpproposal') and self.ljp_to:
return cvt_to_decimal((1-settings.LJP_TAX) * min(settings.LJP_CONTRIBUTION_PER_DAY * self.excursion.ljp_participant_count * self.excursion.ljp_duration,
0.9 * float(self.total_bills_theoretic) + float(self.total_staff)))
else:
return 0
@property @property
def total(self): def total(self):
return self.total_bills + self.total_staff return self.total_bills + self.total_staff + self.paid_ljp_contributions
@property @property
def total_theoretic(self): def total_theoretic(self):
@ -432,6 +453,11 @@ class Statement(CommonModel):
'total_subsidies': self.total_subsidies, 'total_subsidies': self.total_subsidies,
'subsidy_to': self.subsidy_to, 'subsidy_to': self.subsidy_to,
'allowance_to': self.allowance_to, 'allowance_to': self.allowance_to,
'paid_ljp_contributions': self.paid_ljp_contributions,
'ljp_to': self.ljp_to,
'participant_count': self.excursion.participant_count,
'total_seminar_days': self.excursion.total_seminar_days,
'ljp_tax': settings.LJP_TAX * 100,
} }
return dict(context, **excursion_context) return dict(context, **excursion_context)
else: else:

@ -113,6 +113,25 @@
<p>{% blocktrans %}No receivers of the subsidies were provided. Subsidies will not be used.{% endblocktrans %}</p> <p>{% blocktrans %}No receivers of the subsidies were provided. Subsidies will not be used.{% endblocktrans %}</p>
{% endif %} {% endif %}
{% if statement.ljp_to %}
<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
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>
<th>
<td>{% trans "IBAN valid" %}</td>
</th>
<tr>
<td>{{ statement.ljp_to.name }}</td>
<td>{{ statement.ljp_to.iban_valid|render_bool }}</td>
</tr>
</table>
</p>
{% endif %}
{% endif %} {% endif %}
<h2>{% trans "Total" %}</h2> <h2>{% trans "Total" %}</h2>

@ -71,6 +71,16 @@ Zuschüsse und Aufwandsentschädigung werden wie folgt abgerufen:
\end{itemize} \end{itemize}
{% else %} {% else %}
\noindent Für die vorliegende Ausfahrt sind keine Jugendleiter*innen anspruchsberechtigt für Zuschüsse oder Aufwandsentschädigung. \noindent Für die vorliegende Ausfahrt sind keine Jugendleiter*innen anspruchsberechtigt für Zuschüsse oder Aufwandsentschädigung.
{% endif %}
{% if statement.ljp_to %}
\noindent\textbf{LJP-Zuschüsse}
\noindent Der LJP-Zuschuss für die Teilnehmenden in Höhe von {{ statement.paid_ljp_contributions|esc_all }} € wird überwiesen an:
{{ statement.ljp_to.name|esc_all }} Dieser Zuschuss wird aus Landesmitteln gewährt und ist daher
in der Ausgabenübersicht gesondert aufgeführt.
{% endif %} {% endif %}
{% else %} {% else %}
@ -104,11 +114,16 @@ Zuschüsse und Aufwandsentschädigung werden wie folgt abgerufen:
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if statement.subsidy_to %} {% if statement.subsidy_to %}
\multicolumn{2}{l}{Zuschuss Übernachtung und Anreise für alle Jugendleiter*innen} & {{ statement.subsidy_to.name }} & {{ statement.total_subsidies}}\\ \multicolumn{2}{l}{Zuschuss Übernachtung und Anreise für alle Jugendleiter*innen} & {{ statement.subsidy_to.name|esc_all }} & {{ statement.total_subsidies }}\\
{% endif %} {% endif %}
\midrule \midrule
\multicolumn{3}{l}{\textbf{Summe Zuschüsse und Aufwandsentschädigung}} & \textbf{ {{ statement.total_staff }} }\\ \multicolumn{3}{l}{\textbf{Summe Zuschüsse und Aufwandsentschädigung Jugendleitende}} & \textbf{ {{ statement.total_staff }} }\\
{%endif %} {%endif %}
{% if statement.ljp_to %}
\midrule
LJP-Zuschuss für die Teilnehmenden && {{ statement.ljp_to.name|esc_all }} & {{ statement.paid_ljp_contributions|esc_all }}\\
{% endif %}
{% if statement.bills_covered and excursion.approved_staff_count > 0 %} {% if statement.bills_covered and excursion.approved_staff_count > 0 %}
\midrule \midrule
\textbf{Gesamtsumme}& & & \textbf{ {{ statement.total }} }\\ \textbf{Gesamtsumme}& & & \textbf{ {{ statement.total }} }\\

@ -20,6 +20,7 @@ DIGITAL_MAIL = get_var('section', 'digital_mail', default='bar@example.org')
V32_HEAD_ORGANISATION = get_var('LJP', 'v32_head_organisation', default='not configured') V32_HEAD_ORGANISATION = get_var('LJP', 'v32_head_organisation', default='not configured')
LJP_CONTRIBUTION_PER_DAY = get_var('LJP', 'contribution_per_day', default=25) LJP_CONTRIBUTION_PER_DAY = get_var('LJP', 'contribution_per_day', default=25)
LJP_TAX = get_var('LJP', 'tax', default=0)
# echo # echo

@ -898,10 +898,11 @@ class StatementOnListForm(forms.ModelForm):
# of subsidies and allowance # of subsidies and allowance
self.fields['allowance_to'].queryset = excursion.jugendleiter.all() self.fields['allowance_to'].queryset = excursion.jugendleiter.all()
self.fields['subsidy_to'].queryset = excursion.jugendleiter.all() self.fields['subsidy_to'].queryset = excursion.jugendleiter.all()
self.fields['ljp_to'].queryset = excursion.jugendleiter.all()
class Meta: class Meta:
model = Statement model = Statement
fields = ['night_cost', 'allowance_to', 'subsidy_to'] fields = ['night_cost', 'allowance_to', 'subsidy_to', 'ljp_to']
def clean(self): def clean(self):
"""Check if the `allowance_to` and `subsidy_to` fields are compatible with """Check if the `allowance_to` and `subsidy_to` fields are compatible with
@ -922,7 +923,7 @@ class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedIn
extra = 1 extra = 1
description = _('Please list here all expenses in relation with this excursion and upload relevant bills. These have to be permanently stored for the application of LJP contributions. The short descriptions are used in the seminar report cost overview (possible descriptions are e.g. food, material, etc.).') description = _('Please list here all expenses in relation with this excursion and upload relevant bills. These have to be permanently stored for the application of LJP contributions. The short descriptions are used in the seminar report cost overview (possible descriptions are e.g. food, material, etc.).')
sortable_options = [] sortable_options = []
fields = ['night_cost', 'allowance_to', 'subsidy_to'] fields = ['night_cost', 'allowance_to', 'subsidy_to', 'ljp_to']
inlines = [BillOnExcursionInline] inlines = [BillOnExcursionInline]
form = StatementOnListForm form = StatementOnListForm
@ -1238,8 +1239,7 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
opts=self.opts, opts=self.opts,
memberlist=memberlist, memberlist=memberlist,
object=memberlist, object=memberlist,
participant_count=memberlist.participant_count, ljp_contributions=memberlist.payable_ljp_contributions,
ljp_contributions=memberlist.potential_ljp_contributions,
total_relative_costs=memberlist.total_relative_costs, total_relative_costs=memberlist.total_relative_costs,
**memberlist.statement.template_context()) **memberlist.statement.template_context())
return render(request, 'admin/freizeit_finance_overview.html', context=context) return render(request, 'admin/freizeit_finance_overview.html', context=context)

@ -1345,6 +1345,26 @@ msgstr ""
msgid "LJP contributions" msgid "LJP contributions"
msgstr "LJP Zuschüsse" msgstr "LJP Zuschüsse"
#: members/templates/admin/freizeit_finance_overview.html
#, python-format
msgid ""
"By submitting the given seminar report, you will receive LJP contributions. "
"You have\n"
" documented interventions worth of %(total_seminar_days)s seminar days "
"for %(participant_count)s participants.\n"
" This results in a total contribution of %(ljp_contributions)s€. To "
"receive them, you need to \n"
" 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"
" 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 de Ausfahrt beim Jugendreferat einreichen und "
"formal genehmigt bekommen."
#: members/templates/admin/freizeit_finance_overview.html #: members/templates/admin/freizeit_finance_overview.html
#, python-format #, python-format
msgid "" msgid ""
@ -1352,13 +1372,17 @@ msgid ""
"case,\n" "case,\n"
"you may obtain up to 25€ times %(duration)s days for %(participant_count)s " "you may obtain up to 25€ times %(duration)s days for %(participant_count)s "
"participants but only up to\n" "participants but only up to\n"
"90%% of the total costs. This results in a total of %(ljp_contributions)s€." "90%% of the total costs. This results in a total of %(ljp_contributions)s€. "
"If you have created a seminar report, you need to specify who should receive "
"the contributions in order to make use of them."
msgstr "" msgstr ""
"Indem du einen Seminarbericht anfertigst, kannst du Landesjugendplan (LJP) " "Indem du einen Seminarbericht anfertigst, kannst du Landesjugendplan (LJP) "
"Zuschüsse beantragen. In diesem Fall kannst du bis zu 25€ mal %(duration)s " "Zuschüsse beantragen. In diesem Fall kannst du bis zu 25€ mal %(duration)s "
"Tage für %(participant_count)s Teilnehmende, aber nicht mehr als 90%% der " "Tage für %(participant_count)s Teilnehmende, aber nicht mehr als 90%% der "
"Gesamtausgaben erhalten. Das resultiert in einem Gesamtzuschuss von " "Gesamtausgaben erhalten. Das resultiert in einem Gesamtzuschuss von "
"%(ljp_contributions)s€." "%(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 #: members/templates/admin/freizeit_finance_overview.html
msgid "Summary" msgid "Summary"

@ -132,12 +132,24 @@ cost plan!
<h3>{% trans "LJP contributions" %}</h3> <h3>{% trans "LJP contributions" %}</h3>
{% if memberlist.statement.ljp_to %}
<p>
{% 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.
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 %}
</p>
{% else %}
<p> <p>
{% blocktrans %}By submitting a seminar report, you may apply for LJP contributions. In this case, {% blocktrans %}By submitting a seminar report, you may apply for LJP contributions. In this case,
you may obtain up to 25€ times {{ duration }} days for {{ participant_count }} participants but only up to you may obtain up to 25€ times {{ duration }} days for {{ participant_count }} participants but only up to
90% of the total costs. This results in a total of {{ ljp_contributions }}€.{% endblocktrans %} 90% of the total costs. This results in a total of {{ ljp_contributions }}€. If you have created a seminar report, you need to specify who should receive the contributions in order to make use of them.{% endblocktrans %}
</p> </p>
{% endif %}
<h3>{% trans "Summary" %}</h3> <h3>{% trans "Summary" %}</h3>
<p> <p>
@ -163,7 +175,7 @@ you may obtain up to 25€ times {{ duration }} days for {{ participant_count }}
</tr> </tr>
<tr> <tr>
<td> <td>
{% trans "Potential LJP contributions" %} {% if memberlist.statement.ljp_to %}{% trans "LJP contributions" %}{% else %}{% trans "Potential LJP contributions" %}{% endif %}
</td> </td>
<td> <td>
-{{ ljp_contributions }}€ -{{ ljp_contributions }}€

Loading…
Cancel
Save