diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py index 463e552..d9fae6f 100644 --- a/jdav_web/finance/admin.py +++ b/jdav_web/finance/admin.py @@ -206,6 +206,14 @@ class StatementSubmittedAdmin(admin.ModelAdmin): _("Some transactions have no ledger configured. Please fill in the gaps.") % {'name': str(statement)}) 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))) if "reject" in request.POST: diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po index 8738751..3c6ffaf 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-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" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -74,6 +74,22 @@ msgid "Some transactions have no ledger configured. Please fill in the gaps." msgstr "" "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 #, python-format msgid "Successfully rejected %(name)s. The requestor can reapply, when needed." @@ -233,8 +249,13 @@ msgstr "Bereit zur Abwicklung" #: finance/models.py #, python-format -msgid "Compensation for %(excu)s" -msgstr "Entschädigung für %(excu)s" +msgid "Allowance for %(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 msgid "Total" @@ -386,8 +407,8 @@ msgstr "Ausfahrt" #, python-format msgid "This excursion featured %(staff_count)s youth leader(s), each costing" msgstr "" -"Diese Ausfahrt hatte %(staff_count)s Jugendleiter*innen. Auf jede*n " -"entfallen die folgenden Kosten:" +"Diese Ausfahrt hatte %(staff_count)s genehmigte Jugendleiter*innen. Auf " +"jede*n entfallen die folgenden Kosten:" #: finance/templates/admin/overview_submitted_statement.html #, python-format @@ -425,6 +446,22 @@ msgstr "" "Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)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 #, python-format msgid "This results in a total amount of %(total)s€" diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py index ed437e7..81e62b0 100644 --- a/jdav_web/finance/models.py +++ b/jdav_web/finance/models.py @@ -47,7 +47,7 @@ class StatementManager(models.Manager): 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'), 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] 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) 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): return len(self.transaction_issues) == 0 - def is_valid(self): - return self.ledgers_configured and self.transactions_match_expenses - is_valid.boolean = True - is_valid.short_description = _('Ready to confirm') + @property + def allowance_to_valid(self): + """Checks if the configured `allowance_to` field matches the regulations.""" + 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 def validity(self): @@ -168,9 +185,18 @@ class Statement(CommonModel): return Statement.NON_MATCHING_TRANSACTIONS if not self.ledgers_configured: 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: 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): if not self.submitted: return False @@ -203,9 +229,14 @@ class Statement(CommonModel): if self.excursion is None: return True - for yl in self.excursion.jugendleiter.all(): - ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name} - Transaction(statement=self, member=yl, amount=self.real_per_yl, confirmed=False, reference=ref).save() + # allowance + for yl in self.allowance_to.all(): + 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 def reduce_transactions(self): @@ -300,6 +331,14 @@ class Statement(CommonModel): 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 def total_staff(self): return self.total_per_yl * self.real_staff_count @@ -353,6 +392,7 @@ class Statement(CommonModel): 'transportation_per_yl': self.transportation_per_yl, 'total_per_yl': self.total_per_yl, 'total_staff': self.total_staff, + 'total_subsidies': self.total_subsidies, } 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 4fd11f8..4a33ae6 100644 --- a/jdav_web/finance/templates/admin/overview_submitted_statement.html +++ b/jdav_web/finance/templates/admin/overview_submitted_statement.html @@ -73,6 +73,25 @@ {% blocktrans %}In total this is {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %}

+

+{% blocktrans %}The allowance of {{ allowance_per_yl }}€ per person should be paid to:{% endblocktrans %} +

+

+

+{% blocktrans %}The subsidies for night and transportation costs of {{ total_subsidies }}€ should be paid to:{% endblocktrans %} +

+

+ {% endif %}

{% trans "Total" %}