{% trans "Confirm trial group meeting invitation" %}
+
+
+{% blocktrans %}You were invited to a trial group meeting of the group {{ groupname }}.{% endblocktrans %}
+{{ timeinfo }}
+
+
+
+{% blocktrans %}Do you want to take part in the trial group meeting? If yes, please confirm your attendance by clicking on the following button.{% endblocktrans %}
+
+{% blocktrans %}You successfully confirmed the invitation to the trial group meeting of the group {{ groupname }}.{% endblocktrans %}
+{{ timeinfo }}
+
+
+{% blocktrans %}We have informed the group leaders about your confirmation. If for some reason you can not make it,
+please contact the group leaders at{% endblocktrans %}
+{{ contact_email }}.
+
+
+{% endblock %}
diff --git a/jdav_web/members/urls.py b/jdav_web/members/urls.py
index 7390159..1fdedfb 100644
--- a/jdav_web/members/urls.py
+++ b/jdav_web/members/urls.py
@@ -12,6 +12,7 @@ urlpatterns = [
re_path(r'^waitinglist/confirm', views.confirm_waiting , name='confirm_waiting'),
re_path(r'^waitinglist/leave', views.leave_waitinglist , name='leave_waitinglist'),
re_path(r'^waitinglist/invitation/reject', views.reject_invitation , name='reject_invitation'),
+ re_path(r'^waitinglist/invitation/confirm', views.confirm_invitation , name='confirm_invitation'),
re_path(r'^waitinglist', views.register_waiting_list , name='register_waiting_list'),
re_path(r'^mail/confirm', views.confirm_mail , name='confirm_mail'),
]
diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py
index 3cb22ea..7f9abee 100644
--- a/jdav_web/members/views.py
+++ b/jdav_web/members/views.py
@@ -479,6 +479,47 @@ def reject_invitation(request):
return render_reject_invalid(request)
+def render_confirm_invitation(request, invitation):
+ return render(request, 'members/confirm_invitation.html',
+ {'invitation': invitation,
+ 'groupname': invitation.group.name,
+ 'contact_email': invitation.group.contact_email,
+ 'timeinfo': invitation.group.get_time_info()})
+
+
+def render_confirm_invalid(request):
+ return render(request, 'members/confirm_invalid.html')
+
+
+def render_confirm_success(request, invitation):
+ return render(request, 'members/confirm_success.html',
+ {'invitation': invitation,
+ 'groupname': invitation.group.name,
+ 'contact_email': invitation.group.contact_email,
+ 'timeinfo': invitation.group.get_time_info()})
+
+
+def confirm_invitation(request):
+ if request.method == 'GET' and 'key' in request.GET:
+ key = request.GET['key']
+ try:
+ invitation = InvitationToGroup.objects.get(key=key)
+ if invitation.rejected or invitation.is_expired():
+ raise ValueError
+ return render_confirm_invitation(request, invitation)
+ except (ValueError, InvitationToGroup.DoesNotExist):
+ return render_confirm_invalid(request)
+ if request.method != 'POST' or 'key' not in request.POST:
+ return render_confirm_invalid(request)
+ key = request.POST['key']
+ try:
+ invitation = InvitationToGroup.objects.get(key=key)
+ except InvitationToGroup.DoesNotExist:
+ return render_confirm_invalid(request)
+ invitation.confirm()
+ return render_confirm_success(request, invitation)
+
+
def confirm_waiting(request):
if request.method == 'GET' and 'key' in request.GET:
key = request.GET['key']
From 33ab4e481d2dde349ff6fd2907a4b89ebe47a9ee Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Sun, 6 Apr 2025 18:49:19 +0200
Subject: [PATCH 08/18] fix(finance/admin): check existence and permission in
finance admin views
---
jdav_web/finance/admin.py | 35 ++++++++++++++-----
.../finance/locale/de/LC_MESSAGES/django.po | 10 +++++-
2 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py
index 0f5237d..e24b5e9 100644
--- a/jdav_web/finance/admin.py
+++ b/jdav_web/finance/admin.py
@@ -40,6 +40,23 @@ class BillOnStatementInline(CommonAdminInlineMixin, admin.TabularInline):
form = BillOnStatementInlineForm
+def decorate_statement_view(model, perm=None):
+ def decorator(fun):
+ def aux(self, request, object_id):
+ try:
+ statement = model.objects.get(pk=object_id)
+ except model.DoesNotExist:
+ messages.error(request, _('Statement not found.'))
+ return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
+ permitted = self.has_change_permission(request, statement) if not perm else request.user.has_perm(perm)
+ if not permitted:
+ messages.error(request, _('Insufficient permissions.'))
+ return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
+ return fun(self, request, statement)
+ return aux
+ return decorator
+
+
@admin.register(StatementUnSubmitted)
class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'submitted']
@@ -77,8 +94,8 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
]
return custom_urls + urls
- def submit_view(self, request, object_id):
- statement = Statement.objects.get(pk=object_id)
+ @decorate_statement_view(Statement)
+ def submit_view(self, request, statement):
if statement.submitted:
messages.error(request,
_("%(name)s is already submitted.") % {'name': str(statement)})
@@ -89,7 +106,7 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
messages.success(request,
_("Successfully submited %(name)s. The finance department will notify the requestors as soon as possible.") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
-
+
if statement.excursion:
memberlist = statement.excursion
context = dict(self.admin_site.each_context(request),
@@ -183,8 +200,8 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
]
return custom_urls + urls
- def overview_view(self, request, object_id):
- statement = StatementSubmitted.objects.get(pk=object_id)
+ @decorate_statement_view(StatementSubmitted)
+ def overview_view(self, request, statement):
if not statement.submitted:
messages.error(request,
_("%(name)s is not yet submitted.") % {'name': str(statement)})
@@ -259,8 +276,8 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
return render(request, 'admin/overview_submitted_statement.html', context=context)
- def reduce_transactions_view(self, request, object_id):
- statement = StatementSubmitted.objects.get(pk=object_id)
+ @decorate_statement_view(StatementSubmitted)
+ def reduce_transactions_view(self, request, statement):
statement.reduce_transactions()
messages.success(request,
_("Successfully reduced transactions for %(name)s.") % {'name': str(statement)})
@@ -307,8 +324,8 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
]
return custom_urls + urls
- def unconfirm_view(self, request, object_id):
- statement = StatementConfirmed.objects.get(pk=object_id)
+ @decorate_statement_view(StatementConfirmed, perm='finance.may_manage_confirmed_statements')
+ def unconfirm_view(self, request, statement):
if not statement.confirmed:
messages.error(request,
_("%(name)s is not yet confirmed.") % {'name': str(statement)})
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index b4ba049..2a6f1a6 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-02-01 21:11+0100\n"
+"POT-Creation-Date: 2025-04-06 18:46+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -18,6 +18,14 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: finance/admin.py
+msgid "Statement not found."
+msgstr "Abrechnung nicht gefunden."
+
+#: finance/admin.py
+msgid "Insufficient permissions."
+msgstr "Unzureichende Berechtigungen."
+
#: finance/admin.py
#, python-format
msgid "%(name)s is already submitted."
From f213e1177209b64a9149c6e7a405a08873b312f0 Mon Sep 17 00:00:00 2001
From: "marius.klein"
Date: Mon, 7 Apr 2025 00:29:35 +0200
Subject: [PATCH 09/18] feat(finance): creation of receipts for confirmed
statements and payment of LJP contributions (#150)
Confirmed statements now come with automatically generated PDF receipts used for documenting all issued payments.
This PR also adds generation of transactions for LJP contributions and their validation.
closes #92
Co-authored-by: mariusrklein <47218379+mariusrklein@users.noreply.github.com>
Reviewed-on: https://git.jdav-hd.merten.dev/digitales/kompass/pulls/150
Reviewed-by: Christian Merten
Co-authored-by: marius.klein
Co-committed-by: marius.klein
---
jdav_web/finance/admin.py | 42 +++++-
.../finance/locale/de/LC_MESSAGES/django.po | 72 ++++++++-
.../migrations/0009_statement_ljp_to.py | 20 +++
jdav_web/finance/models.py | 57 +++++++-
.../admin/overview_submitted_statement.html | 58 +++++++-
.../templates/finance/statement_summary.tex | 137 ++++++++++++++++++
jdav_web/jdav_web/settings/local.py | 1 +
jdav_web/locale/de/LC_MESSAGES/django.po | 6 +-
jdav_web/members/admin.py | 14 +-
jdav_web/members/excel.py | 2 +-
.../members/locale/de/LC_MESSAGES/django.po | 41 +++++-
jdav_web/members/models.py | 40 ++++-
jdav_web/members/pdf.py | 56 ++++---
.../admin/freizeit_finance_overview.html | 29 +++-
.../templates/members/seminar_report_docx.tex | 2 +-
.../change_form_object_tools.html | 5 +-
16 files changed, 541 insertions(+), 41 deletions(-)
create mode 100644 jdav_web/finance/migrations/0009_statement_ljp_to.py
create mode 100644 jdav_web/finance/templates/finance/statement_summary.tex
diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py
index e24b5e9..f9a9e2a 100644
--- a/jdav_web/finance/admin.py
+++ b/jdav_web/finance/admin.py
@@ -1,4 +1,5 @@
from django.contrib import admin, messages
+from django.utils.safestring import mark_safe
from django import forms
from django.forms import Textarea, ClearableFileInput
from django.http import HttpResponse, HttpResponseRedirect
@@ -13,6 +14,7 @@ from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
from utils import get_member, RestrictedFileField
from rules.contrib.admin import ObjectPermissionsModelAdmin
+from members.pdf import render_tex_with_attachments
from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted, StatementConfirmed,\
StatementUnSubmitted, BillOnStatementProxy
@@ -115,7 +117,7 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
memberlist=memberlist,
object=memberlist,
participant_count=memberlist.participant_count,
- ljp_contributions=memberlist.potential_ljp_contributions,
+ ljp_contributions=memberlist.payable_ljp_contributions,
total_relative_costs=memberlist.total_relative_costs,
**memberlist.statement.template_context())
return render(request, 'admin/freizeit_finance_overview.html', context=context)
@@ -129,12 +131,23 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
class TransactionOnSubmittedStatementInline(admin.TabularInline):
model = Transaction
- fields = ['amount', 'member', 'reference', 'ledger']
+ fields = ['amount', 'member', 'reference', 'text_length_warning', 'ledger']
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
+ readonly_fields = ['text_length_warning']
extra = 0
+ def text_length_warning(self, obj):
+ """Display reference length, warn if exceeds 140 characters."""
+ len_reference = len(obj.reference)
+ len_string = f"{len_reference}/140"
+ if len_reference > 140:
+ return mark_safe(f'{len_string}')
+
+ return len_string
+ text_length_warning.short_description = _("Length")
+
class BillOnSubmittedStatementInline(BillOnStatementInline):
model = BillOnStatementProxy
@@ -217,6 +230,9 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
messages.success(request,
_("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.")
% {'name': str(statement)})
+ download_link = reverse('admin:finance_statementconfirmed_summary', args=(statement.pk,))
+ messages.success(request,
+ mark_safe(_("You can download a receipt.") % {'link': download_link}))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "confirm" in request.POST:
res = statement.validity
@@ -271,6 +287,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
title=_('View submitted statement'),
opts=self.opts,
statement=statement,
+ settings=settings,
transaction_issues=statement.transaction_issues,
**statement.template_context())
@@ -321,6 +338,11 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
wrap(self.unconfirm_view),
name="%s_%s_unconfirm" % (self.opts.app_label, self.opts.model_name),
),
+ path(
+ "/summary/",
+ wrap(self.statement_summary_view),
+ name="%s_%s_summary" % (self.opts.app_label, self.opts.model_name),
+ ),
]
return custom_urls + urls
@@ -348,6 +370,22 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
return render(request, 'admin/unconfirm_statement.html', context=context)
+ @decorate_statement_view(StatementConfirmed, perm='finance.may_manage_confirmed_statements')
+ def statement_summary_view(self, request, statement):
+ if not statement.confirmed:
+ messages.error(request,
+ _("%(name)s is not yet confirmed.") % {'name': str(statement)})
+ return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
+ excursion = statement.excursion
+ context = dict(statement=statement.template_context(), excursion=excursion, settings=settings)
+
+ pdf_filename = f"{excursion.code}_{excursion.name}_Zuschussbeleg" if excursion else f"Abrechnungsbeleg"
+ attachments = [bill.proof.path for bill in statement.bills_covered if bill.proof]
+ return render_tex_with_attachments(pdf_filename, 'finance/statement_summary.tex', context, attachments)
+
+ statement_summary_view.short_description = _('Download summary')
+
+
@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
"""The transaction admin site. This is only used to display transactions. All editing
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index 2a6f1a6..5cadb5a 100644
--- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
@@ -48,6 +48,10 @@ msgstr "Kostenübersicht"
msgid "Submit statement"
msgstr "Rechnung einreichen"
+#: finance/admin.py
+msgid "Length"
+msgstr "Länge"
+
#: finance/admin.py
#, python-format
msgid "%(name)s is not yet submitted."
@@ -69,6 +73,13 @@ msgstr ""
"Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen "
"Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern."
+#: finance/admin.py
+#, python-format
+msgid "You can download a receipt."
+msgstr ""
+"Hier kannst du den Abrechnungsbeleg herunterladen."
+
#: finance/admin.py
msgid "Statement confirmed"
msgstr "Abrechnung abgewickelt"
@@ -160,6 +171,10 @@ msgstr ""
msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen"
+#: finance/admin.py
+msgid "Download summary"
+msgstr "Beleg herunterladen"
+
#: finance/apps.py
msgid "Finance"
msgstr "Finanzen"
@@ -211,6 +226,18 @@ msgstr ""
"Die Person, die die Übernachtungs- und Fahrtkostenzuschüsse erhalten soll. "
"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
msgid "Price per night"
msgstr "Preis pro Nacht"
@@ -270,7 +297,12 @@ 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 finance/templates/admin/overview_submitted_statement.html
+#: finance/models.py
+#, python-format
+msgid "LJP-Contribution %(excu)s"
+msgstr "LJP-Zuschuss %(excu)s"
+
+#: finance/models.py
msgid "Total"
msgstr "Gesamtbetrag"
@@ -509,6 +541,44 @@ msgstr ""
"Keine Empfänger*innen für Sektionszuschüsse angegeben. Es werden daher keine "
"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
+msgid "Summary"
+msgstr "Zusammenfassung"
+
+#: finance/templates/admin/overview_submitted_statement.html
+msgid "Covered bills"
+msgstr "Übernommene Ausgaben"
+
+#: finance/templates/admin/overview_submitted_statement.html
+msgid "Allowance"
+msgstr "Aufwandsentschädigung"
+
+#: finance/templates/admin/overview_submitted_statement.html
+msgid "Contributions by the association"
+msgstr "Sektionszuschüsse"
+
+#: finance/templates/admin/overview_submitted_statement.html
+msgid "ljp contributions"
+msgstr "LJP-Zuschüsse"
+
#: 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/migrations/0009_statement_ljp_to.py b/jdav_web/finance/migrations/0009_statement_ljp_to.py
new file mode 100644
index 0000000..fc13323
--- /dev/null
+++ b/jdav_web/finance/migrations/0009_statement_ljp_to.py
@@ -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'),
+ ),
+ ]
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 5fb1aa9..5765aef 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -70,6 +70,13 @@ class Statement(CommonModel):
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.'))
+ 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)
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()])
if self.subsidy_to:
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)
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}
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
def reduce_transactions(self):
@@ -261,7 +274,7 @@ class Statement(CommonModel):
continue
new_amount = sum((trans.amount for trans in grp))
- new_ref = "\n".join((trans.reference for trans in grp))
+ new_ref = ", ".join((f"{trans.reference} EUR{trans.amount: .2f}" for trans in grp))
Transaction(statement=self, member=member, amount=new_amount, confirmed=False, reference=new_ref,
ledger=ledger).save()
for trans in grp:
@@ -269,12 +282,27 @@ class Statement(CommonModel):
@property
def total_bills(self):
- return sum([bill.amount for bill in self.bill_set.all() if bill.costs_covered])
+ return sum([bill.amount for bill in self.bills_covered])
+
+ @property
+ def bills_covered(self):
+ """Returns the bills that are marked for reimbursement by the finance officer"""
+ return [bill for bill in self.bill_set.all() if bill.costs_covered]
+ @property
+ def bills_without_proof(self):
+ """Returns the bills that lack a proof file"""
+ return [bill for bill in self.bill_set.all() if not bill.proof]
+
@property
def total_bills_theoretic(self):
return sum([bill.amount for bill in self.bill_set.all()])
+ @property
+ def total_bills_not_covered(self):
+ """Returns the sum of bills that are not marked for reimbursement by the finance officer"""
+ return sum([bill.amount for bill in self.bill_set.all()]) - self.total_bills
+
@property
def euro_per_km(self):
if self.excursion is None:
@@ -380,10 +408,23 @@ class Statement(CommonModel):
return 0
else:
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(
+ min(
+ (1-settings.LJP_TAX) * settings.LJP_CONTRIBUTION_PER_DAY * self.excursion.ljp_participant_count * self.excursion.ljp_duration,
+ (1-settings.LJP_TAX) * 0.9 * (float(self.total_bills_not_covered) + float(self.total_staff) ),
+ float(self.total_bills_not_covered)
+ )
+ )
+ else:
+ return 0
@property
def total(self):
- return self.total_bills + self.total_staff
+ return self.total_bills + self.total_staff + self.paid_ljp_contributions
@property
def total_theoretic(self):
@@ -403,6 +444,7 @@ class Statement(CommonModel):
context = {
'total_bills': self.total_bills,
'total_bills_theoretic': self.total_bills_theoretic,
+ 'bills_covered': self.bills_covered,
'total': self.total,
}
if self.excursion:
@@ -421,9 +463,18 @@ class Statement(CommonModel):
'transportation_per_yl': self.transportation_per_yl,
'total_per_yl': self.total_per_yl,
'total_staff': self.total_staff,
+ 'total_allowance': self.total_allowance,
'theoretical_total_staff': self.theoretical_total_staff,
'real_staff_count': self.real_staff_count,
'total_subsidies': self.total_subsidies,
+ 'total_allowance': self.total_allowance,
+ 'subsidy_to': self.subsidy_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)
else:
diff --git a/jdav_web/finance/templates/admin/overview_submitted_statement.html b/jdav_web/finance/templates/admin/overview_submitted_statement.html
index e6196de..0d97736 100644
--- a/jdav_web/finance/templates/admin/overview_submitted_statement.html
+++ b/jdav_web/finance/templates/admin/overview_submitted_statement.html
@@ -113,9 +113,65 @@
{% blocktrans %}No receivers of the subsidies were provided. Subsidies will not be used.{% endblocktrans %}
{% endif %}
+{% if statement.ljp_to %}
+
+{% 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 %}
+
+
+
{% trans "IBAN valid" %}
+
+
+
{{ statement.ljp_to.name }}
+
{{ statement.ljp_to.iban_valid|render_bool }}
+
+
+
{% endif %}
-
{% trans "Total" %}
+
+{% endif %}
+
+
+
{% trans "Summary" %}
+
+
+
+
+
+ {% trans "Covered bills" %}
+
+
+ {{ total_bills }}€
+
+
+
+
+ {% trans "Allowance" %}
+
+
+ {{ total_allowance }}€
+
+
+
+
+ {% trans "Contributions by the association" %}
+
+
+ {{ total_subsidies }}€
+
+
+
+
+ {% trans "ljp contributions" %}
+
+
+ {{ paid_ljp_contributions }}€
+
+
+
{% blocktrans %}This results in a total amount of {{ total }}€{% endblocktrans %}
diff --git a/jdav_web/finance/templates/finance/statement_summary.tex b/jdav_web/finance/templates/finance/statement_summary.tex
new file mode 100644
index 0000000..ee37f02
--- /dev/null
+++ b/jdav_web/finance/templates/finance/statement_summary.tex
@@ -0,0 +1,137 @@
+{% extends "members/tex_base.tex" %}
+{% load static common tex_extras %}
+
+{% block title %}Abrechnungs- und Zuschussbeleg\\[2mm]Sektionsveranstaltung{% endblock %}
+
+{% block content %}
+
+{% if excursion %}
+\noindent\textbf{\large Ausfahrt}
+
+% DESCRIPTION TABLE
+\begin{table}[H]
+ \begin{tabular}{ll}
+ Aktivität: & {{ excursion.name|esc_all }} \\
+ Ordnungsnummer & {{ excursion.code|esc_all }} \\
+ Ort / Stützpunkt: & {{ excursion.place|esc_all }} \\
+ Zeitraum: & {{ excursion.duration|esc_all}} Tage ({{ excursion.time_period_str|esc_all }}) \\
+ Teilnehmer*innen: & {{ excursion.participant_count }} der Gruppe(n) {{ excursion.groups_str|esc_all }} \\
+ Betreuer*innen: & {{excursion.staff_count|esc_all }} ({{ excursion.staff_str|esc_all }}) \\
+ Art der Tour: & {% checked_if_true 'Gemeinschaftstour' excursion.get_tour_type %}
+ {% checked_if_true 'Führungstour' excursion.get_tour_type %}
+ {% checked_if_true 'Ausbildung' excursion.get_tour_type %} \\
+ Anreise: & {% checked_if_true 'ÖPNV' excursion.get_tour_approach %}
+ {% checked_if_true 'Muskelkraft' excursion.get_tour_approach %}
+ {% checked_if_true 'Fahrgemeinschaften' excursion.get_tour_approach %}
+ \end{tabular}
+\end{table}
+
+\noindent\textbf{\large Zuschüsse und Aufwandsentschädigung}
+{% if excursion.approved_staff_count > 0 %}
+
+\noindent Gemäß Beschluss des Jugendausschusses gelten folgende Sätze für Zuschüsse pro genehmigter Jugendleiter*in:
+
+\begin{table}[H]
+ \centering
+ \begin{tabularx}{.97\textwidth}{Xllr}
+ \toprule
+ \textbf{Posten} & \textbf{Einzelsatz} & \textbf{Anzahl} & \textbf{Gesamtbetrag pro JL} \\
+ \midrule
+ Zuschuss Übernachtung & {{ statement.price_per_night }} € / Nacht & {{ statement.nights }} Nächte & {{ statement.nights_per_yl }} € \\
+ Zuschuss Anreise & {{statement.euro_per_km}} € / km ({{ statement.means_of_transport }}) & {{ statement.kilometers_traveled }} km & {{ statement.transportation_per_yl }} € \\
+ Aufwandsentschädigung & {{ statement.allowance_per_day }},00 € / Tag & {{ statement.duration }} Tage & {{ statement.allowance_per_yl }} € \\
+ \midrule
+ \textbf{Summe}& & & \textbf{ {{ statement.total_per_yl }} }€\\
+ \bottomrule
+ \end{tabularx}
+\end{table}
+
+\noindent Gemäß JDAV-Betreuungsschlüssel können bei {{ excursion.participant_count }} Teilnehmer*innen
+bis zu {{ excursion.approved_staff_count }} Jugendleiter*innen {% if excursion.approved_extra_youth_leader_count %}
+(davon {{ excursion.approved_extra_youth_leader_count }} durch das Jugendreferat zusätzlich genehmigt){% endif %} bezuschusst werden.
+Zuschüsse und Aufwandsentschädigung werden wie folgt abgerufen:
+\begin{itemize}
+
+ {% if statement.allowances_paid > 0 %}
+
+ \item Eine Aufwandsentschädigung von {{ statement.allowance_per_yl }} € pro Jugendleiter*in wird überwiesen an:
+ {% for m in statement.allowance_to.all %}{% if forloop.counter > 1 %}, {% endif %}{{ m.name }}{% endfor %}
+ {% else %}
+ \item Keiner*r der Jugendleiter*innen nimmt eine Aufwandsentschädigung in Anspruch.
+ {% endif %}
+
+ {% if statement.subsidy_to %}
+ \item Der Zuschuss zu Übernachtung und Anreise für alle Jugendleiter*innen in Höhe von {{ statement.total_subsidies }} € wird überwiesen an:
+ {{ statement.subsidy_to.name }}
+
+ {% else %}
+ \item Zuschüsse zu Übernachtung und Anreise werden nicht in Anspruch genommen.
+ {% endif %}
+
+\end{itemize}
+{% else %}
+\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 %}
+
+{% else %}
+\vspace{110pt}
+{% endif %}
+
+
+\vspace{12pt}
+
+\noindent\textbf{\large Ausgabenübersicht}
+\nopagebreak
+\begin{table}[H]
+ \centering
+ \begin{tabularx}{.97\textwidth}{lXlr}
+ \toprule
+ \textbf{Titel} & \textbf{Beschreibung} & \textbf{Auszahlung an} & \textbf{Betrag} \\
+ \midrule
+
+{% if statement.bills_covered %}
+ {% for bill in statement.bills_covered %}
+ {{ forloop.counter }}. {{ bill.short_description}} & {{ bill.explanation}} & {{ bill.paid_by.name|esc_all }} & {{ bill.amount }} € \\
+ {% endfor %}
+ \midrule
+ \multicolumn{3}{l}{\textbf{Summe übernommene Ausgaben}} & \textbf{ {{ statement.total_bills }} }€\\
+{% endif %}
+{% if excursion.approved_staff_count > 0 and statement.allowances_paid > 0 or excursion.approved_staff_count > 0 and statement.subsidy_to %}
+ \midrule
+ {% if statement.allowances_paid > 0 %}
+ {% for m in statement.allowance_to.all %}
+ Aufwandsentschädigung & & {{ m.name|esc_all }} & {{ statement.allowance_per_yl }} €\\
+ {% endfor %}
+ {% endif %}
+ {% 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 %}
+ \midrule
+ \multicolumn{3}{l}{\textbf{Summe Zuschüsse und Aufwandsentschädigung Jugendleitende}} & \textbf{ {{ statement.total_staff }} }€\\
+{%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.ljp_to or statement.bills_covered and excursion.approved_staff_count > 0 %}
+ \midrule
+ \textbf{Gesamtsumme}& & & \textbf{ {{ statement.total }} }€\\
+{% endif %}
+ \bottomrule
+ \end{tabularx}
+\end{table}
+
+\noindent Dieser Beleg wird automatisch erstellt und daher nicht unterschrieben.
+
+{% endblock %}
diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py
index 20c8c13..7d7ebcc 100644
--- a/jdav_web/jdav_web/settings/local.py
+++ b/jdav_web/jdav_web/settings/local.py
@@ -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')
LJP_CONTRIBUTION_PER_DAY = get_var('LJP', 'contribution_per_day', default=25)
+LJP_TAX = get_var('LJP', 'tax', default=0)
# echo
diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po
index cafea14..1adcf4e 100644
--- a/jdav_web/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/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-02-01 14:54+0100\n"
+"POT-Creation-Date: 2025-04-06 19:10+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -236,6 +236,10 @@ msgstr "Löschen?"
msgid "Unconfirm"
msgstr "Bestätigung zurücknehmen"
+#: templates/admin/finance/statementconfirmed/change_form_object_tools.html
+msgid "Download summary"
+msgstr "Beleg herunterladen"
+
#: templates/admin/finance/statementsubmitted/change_form_object_tools.html
msgid "Reduce transactions"
msgstr "Überweisungen minimieren"
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index ea45863..c61fb6b 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -898,10 +898,11 @@ class StatementOnListForm(forms.ModelForm):
# of subsidies and allowance
self.fields['allowance_to'].queryset = excursion.jugendleiter.all()
self.fields['subsidy_to'].queryset = excursion.jugendleiter.all()
+ self.fields['ljp_to'].queryset = excursion.jugendleiter.all()
class Meta:
model = Statement
- fields = ['night_cost', 'allowance_to', 'subsidy_to']
+ fields = ['night_cost', 'allowance_to', 'subsidy_to', 'ljp_to']
def clean(self):
"""Check if the `allowance_to` and `subsidy_to` fields are compatible with
@@ -922,7 +923,7 @@ class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedIn
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.).')
sortable_options = []
- fields = ['night_cost', 'allowance_to', 'subsidy_to']
+ fields = ['night_cost', 'allowance_to', 'subsidy_to', 'ljp_to']
inlines = [BillOnExcursionInline]
form = StatementOnListForm
@@ -1229,6 +1230,12 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
messages.error(request,
_("The configured recipients of the allowance don't match the regulations. Please correct this and try again."))
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(memberlist.pk,)))
+
+ if memberlist.statement.ljp_to and len(memberlist.statement.bills_without_proof) > 0:
+ messages.error(request,
+ _("The excursion is configured to claim LJP contributions. In that case, for all bills, a proof must be uploaded. Please correct this and try again."))
+ return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(memberlist.pk,)))
+
memberlist.statement.submit(get_member(request))
messages.success(request,
_("Successfully submited statement. The finance department will notify you as soon as possible."))
@@ -1238,8 +1245,7 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
opts=self.opts,
memberlist=memberlist,
object=memberlist,
- participant_count=memberlist.participant_count,
- ljp_contributions=memberlist.potential_ljp_contributions,
+ ljp_contributions=memberlist.payable_ljp_contributions,
total_relative_costs=memberlist.total_relative_costs,
**memberlist.statement.template_context())
return render(request, 'admin/freizeit_finance_overview.html', context=context)
diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py
index 20bbbc1..d8b55e3 100644
--- a/jdav_web/members/excel.py
+++ b/jdav_web/members/excel.py
@@ -117,7 +117,7 @@ def generate_ljp_vbk(excursion):
sheet['D19'] = settings.SEKTION
sheet['G19'] = title
sheet['I19'] = f"von {excursion.date:%d.%m.%y} bis {excursion.end:%d.%m.%y}"
- sheet['J19'] = excursion.duration
+ sheet['J19'] = excursion.ljp_duration
sheet['L19'] = f"{excursion.ljp_participant_count}"
sheet['H19'] = excursion.get_ljp_activity_category()
sheet['M19'] = f"{excursion.postcode}, {excursion.place}"
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index e2d3d42..2e9381a 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-06 15:38+0200\n"
+"POT-Creation-Date: 2025-04-06 18:57+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -417,6 +417,15 @@ msgstr ""
"Die ausgewählten Empfänger*innen der Aufwandsentschädigung stimmen nicht mit "
"den Richtlinien überein. Bitte korrigiere das und versuche es erneut. "
+#: members/admin.py
+msgid ""
+"The excursion is configured to claim LJP contributions. In that case, for "
+"all bills, a proof must be uploaded. Please correct this and try again."
+msgstr ""
+"Für die Ausfahrt werden LJP-Zuschüsse beantragt. Dafür müssen für alle "
+"Ausgaben Belege hochgeladen werden. Bitte lade für alle Ausgaben einen Beleg "
+"hoch und versuche es erneut. "
+
#: members/admin.py
msgid ""
"Successfully submited statement. The finance department will notify you as "
@@ -1354,6 +1363,28 @@ msgstr ""
msgid "LJP contributions"
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.\n"
+"You have 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€.\n"
+"To receive them, you need to 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 der Ausfahrt beim Jugendreferat einreichen und "
+"formal genehmigt bekommen."
+
+#: members/templates/admin/freizeit_finance_overview.html
+msgid "The LJP contributions are configured to be paid to:"
+msgstr "Die LJP-Zuschüsse werden ausgezahlt an:"
+
#: members/templates/admin/freizeit_finance_overview.html
#, python-format
msgid ""
@@ -1361,13 +1392,17 @@ msgid ""
"case,\n"
"you may obtain up to 25€ times %(duration)s days for %(participant_count)s "
"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 ""
"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 %(participant_count)s Teilnehmende, aber nicht mehr als 90%% der "
"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
msgid "Summary"
diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py
index 7e6eecc..dd617f2 100644
--- a/jdav_web/members/models.py
+++ b/jdav_web/members/models.py
@@ -8,6 +8,7 @@ import csv
from django.db import models
from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\
Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef
+from django.db.models.functions import TruncDate
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.utils.html import format_html
@@ -1278,16 +1279,40 @@ class Freizeit(CommonModel):
else:
return 0
+ @property
+ def total_seminar_days(self):
+ """calculate seminar days based on intervention hours in every day"""
+ # TODO: add tests for this
+ if hasattr(self, 'ljpproposal'):
+ hours_per_day = (
+ self.ljpproposal.intervention_set
+ .annotate(day=TruncDate('date_start')) # Extract the date (without time)
+ .values('day') # Group by day
+ .annotate(total_duration=Sum('duration')) # Sum durations for each day
+ .order_by('day') # Sort results by date
+ )
+ # Calculate the total number of seminar days
+ # Each day is counted as 1 if total_duration is >= 5 hours, as 0.5 if total_duration is >= 2.5
+ # otherwise 0
+ return sum([min(math.floor(h['total_duration']/cvt_to_decimal(2.5))/2, 1) for h in hours_per_day])
+ else:
+ return 0
+
+ @property
+ def ljp_duration(self):
+ """calculate the duration in days for the LJP"""
+ return min(self.duration, self.total_seminar_days)
+
@property
def staff_count(self):
return self.jugendleiter.count()
-
+
@property
def staff_on_memberlist(self):
ps = set(map(lambda x: x.member, self.membersonlist.distinct()))
jls = set(self.jugendleiter.distinct())
return ps.intersection(jls)
-
+
@property
def staff_on_memberlist_count(self):
return len(self.staff_on_memberlist)
@@ -1297,7 +1322,7 @@ class Freizeit(CommonModel):
ps = set(map(lambda x: x.member, self.membersonlist.distinct()))
jls = set(self.jugendleiter.distinct())
return len(ps - jls)
-
+
@property
def head_count(self):
return self.staff_on_memberlist_count + self.participant_count
@@ -1366,12 +1391,19 @@ class Freizeit(CommonModel):
return cvt_to_decimal(min(self.maximal_ljp_contributions,
0.9 * float(self.statement.total_bills_theoretic) + float(self.statement.total_staff)))
+ @property
+ def payable_ljp_contributions(self):
+ """from the requested ljp contributions, a tax may be deducted for risk reduction"""
+ if self.statement.ljp_to:
+ return self.statement.paid_ljp_contributions
+ return cvt_to_decimal(self.potential_ljp_contributions * cvt_to_decimal(1 - settings.LJP_TAX))
+
@property
def total_relative_costs(self):
if not self.statement:
return 0
total_costs = self.statement.total_bills_theoretic
- total_contributions = self.statement.total_subsidies + self.potential_ljp_contributions
+ total_contributions = self.statement.total_subsidies + self.payable_ljp_contributions
return total_costs - total_contributions
@property
diff --git a/jdav_web/members/pdf.py b/jdav_web/members/pdf.py
index fdd82f0..57e414a 100644
--- a/jdav_web/members/pdf.py
+++ b/jdav_web/members/pdf.py
@@ -47,6 +47,23 @@ def render_docx(name, template_path, context, date=None, save_only=False):
return filename_docx
return serve_media(filename_docx, 'application/docx')
+def render_tex_with_attachments(name, template_path, context, attachments, save_only=False):
+
+ rendered_pdf = render_tex(name, template_path, context, save_only=True)
+
+ reader = PdfReader(media_path(rendered_pdf))
+ writer = PdfWriter()
+ writer.append(reader)
+
+ pdf_add_attachments(writer, attachments)
+
+ with open(media_path(rendered_pdf), 'wb') as output_stream:
+ writer.write(output_stream)
+
+ if save_only:
+ return rendered_pdf
+ return serve_pdf(rendered_pdf)
+
def render_tex(name, template_path, context, date=None, save_only=False):
filename = generate_tex(name, template_path, context, date=date)
@@ -73,6 +90,27 @@ def render_tex(name, template_path, context, date=None, save_only=False):
return serve_pdf(filename_pdf)
+def pdf_add_attachments(pdf_writer, attachments):
+ for fp in attachments:
+ try:
+ if fp.endswith(".pdf"):
+ # append pdf directly
+ img_pdf = PdfReader(fp)
+ else:
+ # convert ensures that png files with an alpha channel can be appended
+ img = Image.open(fp).convert("RGB")
+ img_io = BytesIO()
+ img.save(img_io, "pdf")
+ img_io.seek(0)
+ img_pdf = PdfReader(img_io)
+ img_pdf_scaled = scale_pdf_to_a4(img_pdf)
+ pdf_writer.append(img_pdf_scaled)
+
+ except Exception as e:
+ print("Could not add image", fp)
+ print(e)
+
+
def scale_pdf_page_to_a4(page):
A4_WIDTH, A4_HEIGHT = 595, 842
@@ -114,23 +152,7 @@ def fill_pdf_form(name, template_path, fields, attachments=[], date=None, save_o
writer.update_page_form_field_values(None, fields, auto_regenerate=False)
- for fp in attachments:
- try:
- if fp.endswith(".pdf"):
- # append pdf directly
- img_pdf = PdfReader(fp)
- else:
- # convert ensures that png files with an alpha channel can be appended
- img = Image.open(fp).convert("RGB")
- img_io = BytesIO()
- img.save(img_io, "pdf")
- img_io.seek(0)
- img_pdf = PdfReader(img_io)
- img_pdf_scaled = scale_pdf_to_a4(img_pdf)
- writer.append(img_pdf_scaled)
- except Exception as e:
- print("Could not add image", fp)
- print(e)
+ pdf_add_attachments(writer, attachments)
with open(media_path(filename_pdf), 'wb') as output_stream:
writer.write(output_stream)
diff --git a/jdav_web/members/templates/admin/freizeit_finance_overview.html b/jdav_web/members/templates/admin/freizeit_finance_overview.html
index 3a68dfe..aecc588 100644
--- a/jdav_web/members/templates/admin/freizeit_finance_overview.html
+++ b/jdav_web/members/templates/admin/freizeit_finance_overview.html
@@ -130,14 +130,39 @@ cost plan!
{% endif %}
+
+{% if memberlist.statement.ljp_to %}
{% trans "LJP contributions" %}
+
+{% 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 %}
+
+
+
+ {% blocktrans %}The LJP contributions are configured to be paid to:{% endblocktrans %}
+
{% 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
-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 %}
+{% endif %}
+
{% trans "Summary" %}
@@ -163,7 +188,7 @@ you may obtain up to 25€ times {{ duration }} days for {{ participant_count }}
- {% trans "Potential LJP contributions" %}
+ {% if memberlist.statement.ljp_to %}{% trans "LJP contributions" %}{% else %}{% trans "Potential LJP contributions" %}{% endif %}
{{block.super}}
{% endblock %}
From 60942115ca0efe84ef967506e6ecdba601dd25c9 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Mon, 7 Apr 2025 01:36:46 +0200
Subject: [PATCH 10/18] fix(members/rules): check for groups of excursion
instead of groups of member
---
jdav_web/members/rules.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jdav_web/members/rules.py b/jdav_web/members/rules.py
index c45a032..4193f68 100644
--- a/jdav_web/members/rules.py
+++ b/jdav_web/members/rules.py
@@ -60,7 +60,7 @@ def _is_leader(member, excursion):
return False
if member in excursion.jugendleiter.all():
return True
- yl = [ yl for group in member.group.all() for yl in group.leiters.all() ]
+ yl = [ yl for group in excursion.groups.all() for yl in group.leiters.all() ]
return member in yl
From 62a4e37a158a1f0f3d5e80c770b0cde681896b79 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Thu, 10 Apr 2025 23:48:16 +0200
Subject: [PATCH 11/18] feat(members/admin): re-request mail confirmation for
waiters
---
jdav_web/members/admin.py | 15 +++++++++-
.../members/locale/de/LC_MESSAGES/django.po | 30 +++++++++++++++----
2 files changed, 39 insertions(+), 6 deletions(-)
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index c61fb6b..c7971ed 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -643,7 +643,8 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
'confirmed_mail', 'waiting_confirmed', 'sent_reminders')
search_fields = ('prename', 'lastname', 'email')
list_filter = ['confirmed_mail', InvitedToGroupFilter, AgeFilter, 'gender']
- actions = ['ask_for_registration_action', 'ask_for_wait_confirmation']
+ actions = ['ask_for_registration_action', 'ask_for_wait_confirmation',
+ 'request_mail_confirmation', 'request_required_mail_confirmation']
inlines = [InvitationToGroupAdmin]
readonly_fields= ['application_date', 'sent_reminders']
@@ -671,6 +672,18 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
args=(waiter.pk,)))
return ret
+ def request_mail_confirmation(self, request, queryset):
+ for member in queryset:
+ member.request_mail_confirmation()
+ messages.success(request, _("Successfully requested mail confirmation from selected waiters."))
+ request_mail_confirmation.short_description = _('Request mail confirmation from selected waiters.')
+
+ def request_required_mail_confirmation(self, request, queryset):
+ for member in queryset:
+ member.request_mail_confirmation(rerequest=False)
+ messages.success(request, _("Successfully re-requested missing mail confirmations from selected waiters."))
+ request_required_mail_confirmation.short_description = _('Re-request missing mail confirmations from selected waiters.')
+
def get_urls(self):
urls = super().get_urls()
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 2e9381a..1876643 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-06 18:57+0200\n"
+"POT-Creation-Date: 2025-04-10 23:31+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -222,6 +222,27 @@ msgstr "Erfolgreich %(name)s aufgefordert den Wartelistenplatz zu bestätigen."
msgid "Ask selected waiters to confirm their waiting status"
msgstr "Wartende auffordern den Wartelistenplatz zu bestätigen"
+#: members/admin.py
+msgid "Successfully requested mail confirmation from selected waiters."
+msgstr "Aufforderung zur Bestätigung der Email Adresse versendet."
+
+#: members/admin.py
+msgid "Request mail confirmation from selected waiters."
+msgstr "Aufforderung zur Bestätigung der Email Adresse versenden"
+
+#: members/admin.py
+msgid ""
+"Successfully re-requested missing mail confirmations from selected waiters."
+msgstr ""
+"Erinnerung zur Bestätigung von noch nicht bestätigten Email Adressen "
+"versendet."
+
+#: members/admin.py
+msgid "Re-request missing mail confirmations from selected waiters."
+msgstr ""
+"Erinnerung zur Bestätigung von noch nicht bestätigten Email Adressen "
+"versenden."
+
#: members/admin.py
msgid "Offer waiter a place in a group."
msgstr "Personen auf der Warteliste einen Gruppenplatz anbieten."
@@ -1376,10 +1397,9 @@ 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 der Ausfahrt beim Jugendreferat einreichen und "
-"formal genehmigt bekommen."
+"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 der "
+"Ausfahrt beim Jugendreferat einreichen und formal genehmigt bekommen."
#: members/templates/admin/freizeit_finance_overview.html
msgid "The LJP contributions are configured to be paid to:"
From 2fcdf1ee254217dfd41dc63756b7eaa4813af351 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Thu, 10 Apr 2025 23:56:18 +0200
Subject: [PATCH 12/18] feat(members/admin): add unconfirm member action
---
jdav_web/members/admin.py | 8 +++++++-
jdav_web/members/locale/de/LC_MESSAGES/django.po | 11 ++++++++++-
jdav_web/members/models.py | 4 ++++
3 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py
index c7971ed..c33caa8 100644
--- a/jdav_web/members/admin.py
+++ b/jdav_web/members/admin.py
@@ -240,7 +240,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
}
change_form_template = "members/change_member.html"
ordering = ('lastname',)
- actions = ['request_echo', 'invite_as_user_action']
+ actions = ['request_echo', 'invite_as_user_action', 'unconfirm']
list_per_page = 25
form = MemberAdminForm
@@ -409,6 +409,12 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
name_text_or_link.short_description = _('Name')
name_text_or_link.admin_order_field = 'lastname'
+ def unconfirm(self, request, queryset):
+ for member in queryset:
+ member.unconfirm()
+ messages.success(request, _("Successfully unconfirmed selected members."))
+ unconfirm.short_description = _('Unconfirm selected members.')
+
class DemoteToWaiterForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 1876643..095088d 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-10 23:31+0200\n"
+"POT-Creation-Date: 2025-04-10 23:51+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -133,6 +133,15 @@ msgstr "Aktivität"
msgid "Name"
msgstr "Name"
+#: members/admin.py
+msgid "Successfully unconfirmed selected members."
+msgstr ""
+"Ausgewählte Teilnehmer*innen zu unbestätigten Registrierungen zurückgesetzt."
+
+#: members/admin.py
+msgid "Unconfirm selected members."
+msgstr "Ausgewählte Teilnehmer*innen zu unbestätigten Registrierungen zurücksetzen."
+
#: members/admin.py members/tests.py
msgid "Successfully requested mail confirmation from selected registrations."
msgstr "Aufforderung zur Bestätigung der Email Adresse versendet."
diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py
index dd617f2..8d99151 100644
--- a/jdav_web/members/models.py
+++ b/jdav_web/members/models.py
@@ -414,6 +414,10 @@ class Member(Person):
self.save()
return True
+ def unconfirm(self):
+ self.confirmed = False
+ self.save()
+
def unsubscribe(self, key):
if self.unsubscribe_key == key and timezone.now() <\
self.unsubscribe_expire:
From fb14c2f060908d0c4382d1e2cf42b8efceacee23 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Fri, 11 Apr 2025 00:53:45 +0200
Subject: [PATCH 13/18] feat(members/views): show upload registration form
after echo if missing
---
.../members/locale/de/LC_MESSAGES/django.po | 47 +++++++++++++++----
jdav_web/members/models.py | 5 ++
.../members/upload_registration_form.html | 17 +++++++
.../upload_registration_form_success.html | 14 +++++-
jdav_web/members/tests.py | 5 +-
jdav_web/members/views.py | 14 ++++--
6 files changed, 88 insertions(+), 14 deletions(-)
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 095088d..58b13f6 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-10 23:51+0200\n"
+"POT-Creation-Date: 2025-04-11 00:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -140,7 +140,8 @@ msgstr ""
#: members/admin.py
msgid "Unconfirm selected members."
-msgstr "Ausgewählte Teilnehmer*innen zu unbestätigten Registrierungen zurücksetzen."
+msgstr ""
+"Ausgewählte Teilnehmer*innen zu unbestätigten Registrierungen zurücksetzen."
#: members/admin.py members/tests.py
msgid "Successfully requested mail confirmation from selected registrations."
@@ -1787,6 +1788,8 @@ msgstr ""
#: members/templates/members/echo_password.html
#: members/templates/members/echo_success.html
#: members/templates/members/echo_wrong_password.html
+#: members/templates/members/upload_registration_form.html
+#: members/templates/members/upload_registration_form_success.html
msgid "Echo"
msgstr "Rückmeldung"
@@ -2109,6 +2112,25 @@ msgstr ""
"abgelehnt. Wenn ein Platz in einer anderen Gruppe frei wird, erhältst du "
"eine neue Einladung.\n"
+#: members/templates/members/upload_registration_form.html
+#, python-format
+msgid ""
+"Thank you for echoing back, your data was updated. For legal\n"
+"reasons, we also need a signed participation form. In your case, a recent\n"
+"participation form is missing. Please download\n"
+"the pre-filled form, fill in the remaining fields and read the general "
+"conditions. If you agree,\n"
+"please sign the document and upload a scan or image here."
+msgstr ""
+"Danke für das Aktualisieren deiner Daten. Aus rechtlichen Gründen, "
+"benötigen wir zusätzlich ein schriftliches Anmeldeformular. Für dich liegt uns "
+"kein aktuelles Formular vor. Bitte "
+"lade das Formular herunter, "
+"fülle die verbleibenden Felder aus und lese unsere Teilnahmebedingungen. Falls du "
+"zustimmst, unterschreibe bitte das Formular und lade hier einen Scan oder "
+"ein Bild hoch."
+
#: members/templates/members/upload_registration_form.html
#, python-format
msgid ""
@@ -2141,13 +2163,14 @@ msgid "The supplied key for uploading a registration form is invalid."
msgstr "Der verwendete Link zum Hochladen eines Anmeldeformulars ist ungültig."
#: members/templates/members/upload_registration_form_success.html
-#: members/tests.py
-msgid ""
-"Thank you for uploading the registration form. Our team will process your "
-"registration shortly."
+msgid "Thank you for uploading the registration form."
+msgstr "Danke für das Hochladen des Anmeldeformulars."
+
+#: members/templates/members/upload_registration_form_success.html
+msgid "Our team will process your registration shortly."
msgstr ""
-"Danke für das Hochladen des Anmeldeformulars. Unser Jugendleiterteam wird "
-"deine Registrierung so schnell wie möglich bearbeiten."
+"Unser Jugendleiter*innenteam wird deine Registrierung so schnell wie möglich "
+"bearbeiten."
#: members/templates/members/waiting_confirmation_invalid.html
msgid "Waiting confirmation failed"
@@ -2195,6 +2218,14 @@ msgstr ""
msgid "The entered password is wrong."
msgstr "Das eingegebene Passwort ist falsch."
+#: members/tests.py
+msgid ""
+"Thank you for uploading the registration form. Our team will process your "
+"registration shortly."
+msgstr ""
+"Danke für das Hochladen des Anmeldeformulars. Unser Jugendleiterteam wird "
+"deine Registrierung so schnell wie möglich bearbeiten."
+
#: members/tests.py members/views.py
msgid "invalid"
msgstr "ungültig"
diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py
index 8d99151..aedcf49 100644
--- a/jdav_web/members/models.py
+++ b/jdav_web/members/models.py
@@ -354,6 +354,7 @@ class Member(Person):
help_text=_('If the person registered from the waitinglist, this is their application date.'))
objects = MemberManager()
+ all_objects = models.Manager()
@property
def email_fields(self):
@@ -509,6 +510,10 @@ class Member(Person):
# get activity overview
return Freizeit.objects.filter(membersonlist__member=self)
+ def generate_upload_registration_form_key(self):
+ self.upload_registration_form_key = uuid.uuid4().hex
+ self.save()
+
def create_from_registration(self, waiter, group):
"""Given a member, a corresponding waiting-list object and a group, this completes
the registration and requests email confirmations if necessary.
diff --git a/jdav_web/members/templates/members/upload_registration_form.html b/jdav_web/members/templates/members/upload_registration_form.html
index 9846542..3e12646 100644
--- a/jdav_web/members/templates/members/upload_registration_form.html
+++ b/jdav_web/members/templates/members/upload_registration_form.html
@@ -3,19 +3,36 @@
{% load static %}
{% block title %}
+{% if member.confirmed %}
+{% trans "Echo" %}
+{% else %}
{% trans "Registration" %}
+{% endif %}
{% endblock %}
{% block content %}
+{% if member.confirmed %}
+
{% trans "Echo" %}
+{% else %}
{% trans "Register" %}
+{% endif %}
{% url 'members:download_registration_form' as download_url %}
+{% if member.confirmed %}
+
{% blocktrans %}Thank you for echoing back, your data was updated. For legal
+reasons, we also need a signed participation form. In your case, a recent
+participation form is missing. Please download
+the pre-filled form, fill in the remaining fields and read the general conditions. If you agree,
+please sign the document and upload a scan or image here.{% endblocktrans %}
+
+{% else %}
{% blocktrans %}We summarized your registration in our registration
form. Please download it,
fill in the remaining fields and read the general conditions. If you agree,
please sign the document and upload a scan or image here.{% endblocktrans %}
+{% endif %}
{% blocktrans %}If you are not an adult yet, please let someone responsible for you sign the agreement.{% endblocktrans %}
diff --git a/jdav_web/members/templates/members/upload_registration_form_success.html b/jdav_web/members/templates/members/upload_registration_form_success.html
index 30f9aeb..e593f4c 100644
--- a/jdav_web/members/templates/members/upload_registration_form_success.html
+++ b/jdav_web/members/templates/members/upload_registration_form_success.html
@@ -3,14 +3,26 @@
{% load static %}
{% block title %}
+{% if member.confirmed %}
+{% trans "Echo" %}
+{% else %}
{% trans "Registration" %}
+{% endif %}
{% endblock %}
{% block content %}
+{% if member.confirmed %}
+
{% trans "Echo" %}
+{% else %}
{% trans "Register" %}
+{% endif %}
-
{% blocktrans %}Thank you for uploading the registration form. Our team will process your registration shortly.{% endblocktrans %}
+
+{% blocktrans %}Thank you for uploading the registration form.{% endblocktrans %}
+{% if not member.confirmed %}
+{% blocktrans %}Our team will process your registration shortly.{% endblocktrans %}
+{% endif %}
{% endblock %}
diff --git a/jdav_web/members/tests.py b/jdav_web/members/tests.py
index 3e90a67..55c7211 100644
--- a/jdav_web/members/tests.py
+++ b/jdav_web/members/tests.py
@@ -1354,7 +1354,7 @@ class UploadRegistrationFormViewTestCase(BasicMemberTestCase):
})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response,
- _("Thank you for uploading the registration form. Our team will process your registration shortly."))
+ _("Our team will process your registration shortly."))
class DownloadRegistrationFormViewTestCase(BasicMemberTestCase):
def setUp(self):
@@ -1597,6 +1597,9 @@ class EchoViewTestCase(BasicMemberTestCase):
def setUp(self):
super().setUp()
self.key = self.fritz.generate_echo_key()
+ file = SimpleUploadedFile("form.pdf", b"file_content", content_type="application/pdf")
+ self.fritz.registration_form = file
+ self.fritz.save()
def _assert_failed(self, response):
self.assertEqual(response.status_code, HTTPStatus.OK)
diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py
index 7f9abee..a4789d6 100644
--- a/jdav_web/members/views.py
+++ b/jdav_web/members/views.py
@@ -180,7 +180,13 @@ def echo(request):
# member.echo_key, member.echo_expire = "", timezone.now()
member.echoed = True
member.save()
- return render_echo_success(request, member.prename)
+ if not member.registration_form:
+ # If the member does not have a registration form, forward them to the upload page.
+ member.generate_upload_registration_form_key()
+ member.send_upload_registration_form_link()
+ return HttpResponseRedirect(reverse('members:upload_registration_form') + "?key=" + member.upload_registration_form_key)
+ else:
+ return render_echo_success(request, member.prename)
except ValueError:
# when input is invalid
form = MemberForm(request.POST)
@@ -305,7 +311,7 @@ def download_registration_form(request):
return render_upload_registration_form_invalid(request)
key = request.GET['key']
try:
- member = MemberUnconfirmedProxy.objects.get(upload_registration_form_key=key)
+ member = Member.all_objects.get(upload_registration_form_key=key)
return render_download_registration_form(request, member)
except Member.DoesNotExist:
return render_upload_registration_form_invalid(request)
@@ -332,7 +338,7 @@ def upload_registration_form(request):
return render_upload_registration_form_invalid(request)
key = request.GET['key']
try:
- member = MemberUnconfirmedProxy.objects.get(upload_registration_form_key=key)
+ member = Member.all_objects.get(upload_registration_form_key=key)
except Member.DoesNotExist:
return render_upload_registration_form_invalid(request)
form = UploadRegistrationForm(instance=member)
@@ -341,7 +347,7 @@ def upload_registration_form(request):
return render_upload_registration_form_invalid(request)
key = request.POST['key']
try:
- member = MemberUnconfirmedProxy.objects.get(upload_registration_form_key=key)
+ member = Member.all_objects.get(upload_registration_form_key=key)
except Member.DoesNotExist:
return render_upload_registration_form_invalid(request)
From 6b32595a5f86dc005126f2112aa6974cf9c15047 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Fri, 11 Apr 2025 01:01:45 +0200
Subject: [PATCH 14/18] chore(members/views): modify texts and button size
---
.../members/locale/de/LC_MESSAGES/django.po | 40 +++++++++----------
jdav_web/members/templates/members/echo.html | 2 +-
.../templates/members/member_form.html | 2 +-
3 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 58b13f6..108b36f 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-11 00:39+0200\n"
+"POT-Creation-Date: 2025-04-11 00:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -1793,11 +1793,13 @@ msgstr ""
msgid "Echo"
msgstr "Rückmeldung"
-#: members/templates/members/echo.html members/tests.py
-msgid "Thanks for echoing back. Here is your current data:"
+#: members/templates/members/echo.html
+msgid ""
+"Here is your current data. Please check if it is up to date and change "
+"accordingly."
msgstr ""
-"Vielen Dank, dass du dich rückmeldest. Hier siehst du deine aktuellen Daten. "
-"Falls sich etwas geändert hat, trage das bitte hier ein."
+"Hier siehst du deine aktuellen Daten. Bitte überprüfe alles und passe es "
+"bei Bedarf an."
#: members/templates/members/echo_failed.html members/tests.py
msgid "Echo failed"
@@ -2123,13 +2125,12 @@ msgid ""
"conditions. If you agree,\n"
"please sign the document and upload a scan or image here."
msgstr ""
-"Danke für das Aktualisieren deiner Daten. Aus rechtlichen Gründen, "
-"benötigen wir zusätzlich ein schriftliches Anmeldeformular. Für dich liegt uns "
-"kein aktuelles Formular vor. Bitte "
-"lade das Formular herunter, "
-"fülle die verbleibenden Felder aus und lese unsere Teilnahmebedingungen. Falls du "
-"zustimmst, unterschreibe bitte das Formular und lade hier einen Scan oder "
-"ein Bild hoch."
+"Danke für das Aktualisieren deiner Daten. Aus rechtlichen Gründen, benötigen "
+"wir zusätzlich ein schriftliches Anmeldeformular. Für dich liegt uns kein "
+"aktuelles Formular vor. Bitte lade "
+"das Formular herunter, fülle die verbleibenden Felder aus und lese "
+"unsere Teilnahmebedingungen. Falls du zustimmst, unterschreibe bitte das "
+"Formular und lade hier einen Scan oder ein Bild hoch."
#: members/templates/members/upload_registration_form.html
#, python-format
@@ -2167,6 +2168,7 @@ msgid "Thank you for uploading the registration form."
msgstr "Danke für das Hochladen des Anmeldeformulars."
#: members/templates/members/upload_registration_form_success.html
+#: members/tests.py
msgid "Our team will process your registration shortly."
msgstr ""
"Unser Jugendleiter*innenteam wird deine Registrierung so schnell wie möglich "
@@ -2218,14 +2220,6 @@ msgstr ""
msgid "The entered password is wrong."
msgstr "Das eingegebene Passwort ist falsch."
-#: members/tests.py
-msgid ""
-"Thank you for uploading the registration form. Our team will process your "
-"registration shortly."
-msgstr ""
-"Danke für das Hochladen des Anmeldeformulars. Unser Jugendleiterteam wird "
-"deine Registrierung so schnell wie möglich bearbeiten."
-
#: members/tests.py members/views.py
msgid "invalid"
msgstr "ungültig"
@@ -2234,6 +2228,12 @@ msgstr "ungültig"
msgid "expired"
msgstr "abgelaufen"
+#: members/tests.py
+msgid "Thanks for echoing back. Here is your current data:"
+msgstr ""
+"Vielen Dank, dass du dich rückmeldest. Hier siehst du deine aktuellen Daten. "
+"Falls sich etwas geändert hat, trage das bitte hier ein."
+
#: members/views.py
msgid "Prename of the member."
msgstr "Vorname des*der Teilnehmenden"
diff --git a/jdav_web/members/templates/members/echo.html b/jdav_web/members/templates/members/echo.html
index b12b38f..3737663 100644
--- a/jdav_web/members/templates/members/echo.html
+++ b/jdav_web/members/templates/members/echo.html
@@ -12,7 +12,7 @@
{% trans "Echo" %}
-
{% trans "Thanks for echoing back. Here is your current data:" %}
+
{% trans "Here is your current data. Please check if it is up to date and change accordingly." %}
From fd4770d295619fc5581ed40491e28924eb3eb1c2 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Fri, 11 Apr 2025 01:35:46 +0200
Subject: [PATCH 15/18] fix(members/tests): adapt to new texts
---
jdav_web/members/tests.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/jdav_web/members/tests.py b/jdav_web/members/tests.py
index 55c7211..3e69705 100644
--- a/jdav_web/members/tests.py
+++ b/jdav_web/members/tests.py
@@ -1641,7 +1641,7 @@ class EchoViewTestCase(BasicMemberTestCase):
url = reverse('members:echo')
response = self.client.post(url, data={'key': self.key, 'password': self.fritz.echo_password})
self.assertEqual(response.status_code, HTTPStatus.OK)
- self.assertContains(response, _('Thanks for echoing back. Here is your current data:'))
+ self.assertContains(response, _('Here is your current data. Please check if it is up to date and change accordingly.'))
def test_post_save(self):
url = reverse('members:echo')
@@ -1653,7 +1653,7 @@ class EchoViewTestCase(BasicMemberTestCase):
save='',
))
self.assertEqual(response.status_code, HTTPStatus.OK)
- self.assertContains(response, _('Thanks for echoing back. Here is your current data:'))
+ self.assertContains(response, _('Here is your current data. Please check if it is up to date and change accordingly.'))
# provide everything correctly
url = reverse('members:echo')
From 05f924cdef367575d3f2044529158404420922aa Mon Sep 17 00:00:00 2001
From: "marius.klein"
Date: Sun, 27 Apr 2025 21:20:25 +0200
Subject: [PATCH 16/18] fix(members/excursion): fix seminar day calculation and
add verbosity in finance overview (#153)
Reviewed-on: https://git.jdav-hd.merten.dev/digitales/kompass/pulls/153
Reviewed-by: Christian Merten
Co-authored-by: marius.klein
Co-committed-by: marius.klein
---
jdav_web/finance/models.py | 5 +++
.../members/locale/de/LC_MESSAGES/django.po | 43 +++++++++++++------
jdav_web/members/models.py | 34 +++++++++++----
.../admin/freizeit_finance_overview.html | 28 +++++++++++-
4 files changed, 87 insertions(+), 23 deletions(-)
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 5765aef..92a4c08 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -412,6 +412,10 @@ class Statement(CommonModel):
@property
def paid_ljp_contributions(self):
if hasattr(self.excursion, 'ljpproposal') and self.ljp_to:
+
+ if self.excursion.theoretic_ljp_participant_count < 5:
+ return 0
+
return cvt_to_decimal(
min(
(1-settings.LJP_TAX) * settings.LJP_CONTRIBUTION_PER_DAY * self.excursion.ljp_participant_count * self.excursion.ljp_duration,
@@ -472,6 +476,7 @@ class Statement(CommonModel):
'allowance_to': self.allowance_to,
'paid_ljp_contributions': self.paid_ljp_contributions,
'ljp_to': self.ljp_to,
+ 'theoretic_ljp_participant_count': self.excursion.theoretic_ljp_participant_count,
'participant_count': self.excursion.participant_count,
'total_seminar_days': self.excursion.total_seminar_days,
'ljp_tax': settings.LJP_TAX * 100,
diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po
index 108b36f..4d6f6dd 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-11 00:56+0200\n"
+"POT-Creation-Date: 2025-04-15 22:36+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -1411,6 +1411,18 @@ msgstr ""
"Um den zu erhalten, musst du den LJP-Antrag innerhalb von 3 Wochen nach der "
"Ausfahrt beim Jugendreferat einreichen und formal genehmigt bekommen."
+#: members/templates/admin/freizeit_finance_overview.html
+msgid "Seminar hours"
+msgstr "Seminar-Stunden"
+
+#: members/templates/admin/freizeit_finance_overview.html
+msgid "Seminar days"
+msgstr "Seminar-Tage"
+
+#: members/templates/admin/freizeit_finance_overview.html
+msgid "Sum"
+msgstr "Summe"
+
#: members/templates/admin/freizeit_finance_overview.html
msgid "The LJP contributions are configured to be paid to:"
msgstr "Die LJP-Zuschüsse werden ausgezahlt an:"
@@ -1420,20 +1432,31 @@ msgstr "Die LJP-Zuschüsse werden ausgezahlt an:"
msgid ""
"By submitting a seminar report, you may apply for LJP contributions. In this "
"case,\n"
-"you may obtain up to 25€ times %(duration)s days for %(participant_count)s "
-"participants but only up to\n"
+"you may obtain up to 25€ times %(duration)s days for "
+"%(theoretic_ljp_participant_count)s participants but only up to\n"
"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 ""
"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 %(participant_count)s Teilnehmende, aber nicht mehr als 90%% der "
+"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
+#, python-format
+msgid ""
+" Warning: LJP contributions can only be claimed for activities with at least "
+"5 participants and one leader. This activity currently has only "
+"%(theoretic_ljp_participant_count)s participants."
+msgstr ""
+"Achtung: Nur für Aktivitäten mit mindestens 5 Teilnehmenden und einer "
+"Leitungsperson kann ein LJP-Antrag gestellt werden. Diese Ausfahrt hat "
+"aktuell nur %(theoretic_ljp_participant_count)s Teilnehmende."
+
#: members/templates/admin/freizeit_finance_overview.html
msgid "Summary"
msgstr "Zusammenfassung"
@@ -1793,13 +1816,13 @@ msgstr ""
msgid "Echo"
msgstr "Rückmeldung"
-#: members/templates/members/echo.html
+#: members/templates/members/echo.html members/tests.py
msgid ""
"Here is your current data. Please check if it is up to date and change "
"accordingly."
msgstr ""
-"Hier siehst du deine aktuellen Daten. Bitte überprüfe alles und passe es "
-"bei Bedarf an."
+"Hier siehst du deine aktuellen Daten. Bitte überprüfe alles und passe es bei "
+"Bedarf an."
#: members/templates/members/echo_failed.html members/tests.py
msgid "Echo failed"
@@ -2228,12 +2251,6 @@ msgstr "ungültig"
msgid "expired"
msgstr "abgelaufen"
-#: members/tests.py
-msgid "Thanks for echoing back. Here is your current data:"
-msgstr ""
-"Vielen Dank, dass du dich rückmeldest. Hier siehst du deine aktuellen Daten. "
-"Falls sich etwas geändert hat, trage das bitte hier ein."
-
#: members/views.py
msgid "Prename of the member."
msgstr "Vorname des*der Teilnehmenden"
diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py
index aedcf49..16ec653 100644
--- a/jdav_web/members/models.py
+++ b/jdav_web/members/models.py
@@ -8,7 +8,7 @@ import csv
from django.db import models
from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\
Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef
-from django.db.models.functions import TruncDate
+from django.db.models.functions import Cast
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.utils.html import format_html
@@ -1293,20 +1293,36 @@ class Freizeit(CommonModel):
"""calculate seminar days based on intervention hours in every day"""
# TODO: add tests for this
if hasattr(self, 'ljpproposal'):
- hours_per_day = (
- self.ljpproposal.intervention_set
- .annotate(day=TruncDate('date_start')) # Extract the date (without time)
- .values('day') # Group by day
- .annotate(total_duration=Sum('duration')) # Sum durations for each day
- .order_by('day') # Sort results by date
- )
+ hours_per_day = self.seminar_time_per_day
# Calculate the total number of seminar days
# Each day is counted as 1 if total_duration is >= 5 hours, as 0.5 if total_duration is >= 2.5
# otherwise 0
- return sum([min(math.floor(h['total_duration']/cvt_to_decimal(2.5))/2, 1) for h in hours_per_day])
+ sum_days = sum([h['sum_days'] for h in hours_per_day])
+
+ return sum_days
else:
return 0
+
+ @property
+ def seminar_time_per_day(self):
+ if hasattr(self, 'ljpproposal'):
+ return (
+ self.ljpproposal.intervention_set
+ .annotate(day=Cast('date_start', output_field=models.DateField())) # Force it to date
+ .values('day') # Group by day
+ .annotate(total_duration=Sum('duration'))# Sum durations for each day
+ .annotate(
+ sum_days=Case(
+ When(total_duration__gte=5.0, then=Value(1.0)),
+ When(total_duration__gte=2.5, then=Value(0.5)),
+ default=Value(0.0),)
+ )
+ .order_by('day') # Sort results by date
+ )
+ else:
+ return []
+
@property
def ljp_duration(self):
"""calculate the duration in days for the LJP"""
diff --git a/jdav_web/members/templates/admin/freizeit_finance_overview.html b/jdav_web/members/templates/admin/freizeit_finance_overview.html
index aecc588..31c98e6 100644
--- a/jdav_web/members/templates/admin/freizeit_finance_overview.html
+++ b/jdav_web/members/templates/admin/freizeit_finance_overview.html
@@ -141,6 +141,26 @@ 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 %}
+
+
+
+
{% trans "Seminar hours" %}
+
{% trans "Seminar days" %}
+
+{% for day in memberlist.seminar_time_per_day %}
+
+
{{ day.day }}
+
{{ day.total_duration }}
+
{{ day.sum_days }}
+
+ {% endfor %}
+
+
{% trans "Sum" %}
+
+
{{ total_seminar_days }}
+
+
+
{% blocktrans %}The LJP contributions are configured to be paid to:{% endblocktrans %}
@@ -157,12 +177,18 @@ To receive them, you need to submit the LJP-Proposal within 3 weeks after your e
{% else %}
{% 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 {{ theoretic_ljp_participant_count }} participants but only up to
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 %}
{% endif %}
+{% if memberlist.theoretic_ljp_participant_count < 5 %}
+
+ {% blocktrans %} Warning: LJP contributions can only be claimed for activities with at least 5 participants and one leader. This activity currently has only {{ theoretic_ljp_participant_count }} participants.{% endblocktrans %}
+
+{% endif %}
+
{% trans "Summary" %}
From 3250dc7089ab31c75628da38a8b6b94d23d98509 Mon Sep 17 00:00:00 2001
From: "marius.klein"
Date: Mon, 28 Apr 2025 09:19:02 +0200
Subject: [PATCH 17/18] 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 }}€