finance: adapt to allowance_to and subsidy_to

Use the new `allowance_to` and `subsidy_to` fields in statement validation. The
night and travel costs have to be transferred to the person listed as `subsidy_to`,
the allowance has to be paid to the persons listed in `allowance_to`.
pull/103/head
Christian Merten 11 months ago
parent 17bf6e8186
commit 5d0aa18dde
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -206,6 +206,14 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
_("Some transactions have no ledger configured. Please fill in the gaps.") _("Some transactions have no ledger configured. Please fill in the gaps.")
% {'name': str(statement)}) % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
elif res == Statement.INVALID_ALLOWANCE_TO:
messages.error(request,
_("The configured recipients for the allowance don't match the regulations. Please correct this on the excursion."))
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
elif res == Statement.INVALID_TOTAL:
messages.error(request,
_("The calculated total amount does not match the sum of all transactions. This is most likely a bug."))
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "reject" in request.POST: if "reject" in request.POST:

@ -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-01-18 20:19+0100\n" "POT-Creation-Date: 2025-01-18 22:42+0100\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"
@ -74,6 +74,22 @@ msgid "Some transactions have no ledger configured. Please fill in the gaps."
msgstr "" msgstr ""
"Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach." "Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach."
#: finance/admin.py
msgid ""
"The configured recipients for the allowance don't match the regulations. "
"Please correct this on the excursion."
msgstr ""
"Die ausgewählten Empfänger*innen der Aufwandsentschädigungen entsprechen "
"nicht den Regularien. Bitte korrigiere das in der Ausfahrt."
#: finance/admin.py
msgid ""
"The calculated total amount does not match the sum of all transactions. This "
"is most likely a bug."
msgstr ""
"Der berechnete Gesamtbetrag stimmt nicht mit der Summe aller Überweisungen "
"überein. Das ist höchstwahrscheinlich ein Fehler in der Implementierung."
#: finance/admin.py #: finance/admin.py
#, python-format #, python-format
msgid "Successfully rejected %(name)s. The requestor can reapply, when needed." msgid "Successfully rejected %(name)s. The requestor can reapply, when needed."
@ -233,8 +249,13 @@ msgstr "Bereit zur Abwicklung"
#: finance/models.py #: finance/models.py
#, python-format #, python-format
msgid "Compensation for %(excu)s" msgid "Allowance for %(excu)s"
msgstr "Entschädigung für %(excu)s" msgstr "Aufwandsentschädigung für %(excu)s"
#: finance/models.py
#, python-format
msgid "Night and travel costs for %(excu)s"
msgstr "Übernachtungs- und Fahrtkosten für %(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"
@ -386,8 +407,8 @@ msgstr "Ausfahrt"
#, python-format #, python-format
msgid "This excursion featured %(staff_count)s youth leader(s), each costing" msgid "This excursion featured %(staff_count)s youth leader(s), each costing"
msgstr "" msgstr ""
"Diese Ausfahrt hatte %(staff_count)s Jugendleiter*innen. Auf jede*n " "Diese Ausfahrt hatte %(staff_count)s genehmigte Jugendleiter*innen. Auf "
"entfallen die folgenden Kosten:" "jede*n entfallen die folgenden Kosten:"
#: finance/templates/admin/overview_submitted_statement.html #: finance/templates/admin/overview_submitted_statement.html
#, python-format #, python-format
@ -425,6 +446,22 @@ msgstr ""
"Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, " "Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, "
"insgesamt also %(total_staff)s€." "insgesamt also %(total_staff)s€."
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
msgid "The allowance of %(allowance_per_yl)s€ per person should be paid to:"
msgstr ""
"Die Aufwandsentschädigung von %(allowance_per_yl)s€ pro Person soll "
"ausgezahlt werden an:"
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
msgid ""
"The subsidies for night and transportation costs of %(total_subsidies)s€ "
"should be paid to:"
msgstr ""
"Die Zuschüsse für Übernachtungs- und Fahrtkosten von %(total_subsidies)s€ "
"sollen ausgezahlt 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€"

@ -47,7 +47,7 @@ class StatementManager(models.Manager):
class Statement(CommonModel): class Statement(CommonModel):
MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, VALID = 0, 1, 2 MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, INVALID_ALLOWANCE_TO, INVALID_TOTAL, VALID = 0, 1, 2, 3, 4
short_description = models.CharField(verbose_name=_('Short description'), short_description = models.CharField(verbose_name=_('Short description'),
max_length=30, max_length=30,
@ -123,7 +123,9 @@ class Statement(CommonModel):
needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered and b.paid_by] needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered and b.paid_by]
if self.excursion is not None: if self.excursion is not None:
needed_paiments.extend([(yl, self.real_per_yl) for yl in self.excursion.jugendleiter.all()]) 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))
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])))
@ -157,10 +159,25 @@ class Statement(CommonModel):
def transactions_match_expenses(self): def transactions_match_expenses(self):
return len(self.transaction_issues) == 0 return len(self.transaction_issues) == 0
def is_valid(self): @property
return self.ledgers_configured and self.transactions_match_expenses def allowance_to_valid(self):
is_valid.boolean = True """Checks if the configured `allowance_to` field matches the regulations."""
is_valid.short_description = _('Ready to confirm') if self.allowance_to.count() != self.real_staff_count:
return False
if self.excursion is not None:
yls = self.excursion.jugendleiter.all()
for yl in self.allowance_to.all():
if yl not in yls:
return False
return True
@property
def total_valid(self):
"""Checks if the calculated total agrees with the total amount of all transactions."""
total_transactions = 0
for transaction in self.transaction_set.all():
total_transactions += transaction.amount
return self.total == total_transactions
@property @property
def validity(self): def validity(self):
@ -168,9 +185,18 @@ class Statement(CommonModel):
return Statement.NON_MATCHING_TRANSACTIONS return Statement.NON_MATCHING_TRANSACTIONS
if not self.ledgers_configured: if not self.ledgers_configured:
return Statement.MISSING_LEDGER return Statement.MISSING_LEDGER
if not self.allowance_to_valid:
return Statement.INVALID_ALLOWANCE_TO
if not self.total_valid:
return Statement.INVALID_TOTAL
else: else:
return Statement.VALID return Statement.VALID
def is_valid(self):
return self.validity == Statement.VALID
is_valid.boolean = True
is_valid.short_description = _('Ready to confirm')
def confirm(self, confirmer=None): def confirm(self, confirmer=None):
if not self.submitted: if not self.submitted:
return False return False
@ -203,9 +229,14 @@ class Statement(CommonModel):
if self.excursion is None: if self.excursion is None:
return True return True
for yl in self.excursion.jugendleiter.all(): # allowance
ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name} for yl in self.allowance_to.all():
Transaction(statement=self, member=yl, amount=self.real_per_yl, confirmed=False, reference=ref).save() ref = _("Allowance for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=yl, amount=self.allowance_per_yl, confirmed=False, reference=ref).save()
# subsidies (i.e. night and transportation costs)
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()
return True return True
def reduce_transactions(self): def reduce_transactions(self):
@ -300,6 +331,14 @@ class Statement(CommonModel):
return cvt_to_decimal(self.total_staff / self.excursion.staff_count) return cvt_to_decimal(self.total_staff / self.excursion.staff_count)
@property
def total_subsidies(self):
"""
The total amount of subsidies excluding the allowance, i.e. the transportation
and night costs per youth leader multiplied with the real number of youth leaders.
"""
return (self.transportation_per_yl + self.nights_per_yl) * self.real_staff_count
@property @property
def total_staff(self): def total_staff(self):
return self.total_per_yl * self.real_staff_count return self.total_per_yl * self.real_staff_count
@ -353,6 +392,7 @@ class Statement(CommonModel):
'transportation_per_yl': self.transportation_per_yl, 'transportation_per_yl': self.transportation_per_yl,
'total_per_yl': self.total_per_yl, 'total_per_yl': self.total_per_yl,
'total_staff': self.total_staff, 'total_staff': self.total_staff,
'total_subsidies': self.total_subsidies,
} }
return dict(context, **excursion_context) return dict(context, **excursion_context)
else: else:

@ -73,6 +73,25 @@
{% blocktrans %}In total this is {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %} {% blocktrans %}In total this is {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %}
</p> </p>
<p>
{% blocktrans %}The allowance of {{ allowance_per_yl }}€ per person should be paid to:{% endblocktrans %}
<ul>
{% for member in statement.allowance_to.all %}
<li>
{{ member.name }}
</li>
{% endfor %}
</ul>
</p>
<p>
{% blocktrans %}The subsidies for night and transportation costs of {{ total_subsidies }}€ should be paid to:{% endblocktrans %}
<ul>
<li>
{{ statement.subsidy_to.name }}
</li>
</ul>
</p>
{% endif %} {% endif %}
<h2>{% trans "Total" %}</h2> <h2>{% trans "Total" %}</h2>

Loading…
Cancel
Save