Merge branch 'main' into MK/finance_workflow

MK/finance_workflow
Christian Merten 8 months ago
commit 3afcc25bc4
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -42,6 +42,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']
@ -79,8 +96,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)})
@ -196,8 +213,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)})
@ -276,8 +293,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)})
@ -329,8 +346,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)})

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-06 16:58+0200\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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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."
@ -163,10 +171,6 @@ msgstr ""
msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen"
#: finance/admin.py
msgid "Statement not found."
msgstr "Abrechnung existiert nicht."
#: finance/admin.py
msgid "Download summary"
msgstr "Beleg herunterladen"

@ -40,6 +40,28 @@ aber weiterhin auf der Warteliste.
Viele Grüße
Dein KOMPASS""")
GROUP_INVITATION_CONFIRMED_TEXT = get_text('group_invitation_confirmed',
default="""Hallo {name},
{waiter} hat die Einladung zu einer Schnupperstunde bei der Gruppe {group} angenommen.
Viele Grüße
Dein KOMPASS""")
TRIAL_GROUP_MEETING_CONFIRMED_TEXT = get_text('trial_group_meeting_confirmed',
default="""Hallo {name},
deine Teilnahme an der Schnupperstunde der Gruppe {group} wurde erfolgreich bestätigt.
{timeinfo}
Für alle weiteren Absprachen, kontaktiere bitte die Jugendleiter*innen der Gruppe
unter {contact_email}.
Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION })
GROUP_TIME_AVAILABLE_TEXT = get_text('group_time_available',
default="""Die Gruppenstunde findet jeden {weekday} von {start_time} bis {end_time} Uhr statt.""")
@ -51,7 +73,11 @@ INVITE_TEXT = get_text('invite', default="""Hallo {{name}},
wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden.
{group_time}
Bitte kontaktiere die Gruppenleitung ({contact_email}) für alle weiteren Absprachen.
Wenn du an der Schnupperstunde teilnehmen möchtest, bestätige deine Teilnahme bitte unter folgendem Link:
{{invitation_confirm_link}}
Für alle weiteren Absprachen, kontaktiere bitte die Gruppenleitung ({contact_email}).
Wenn du nach der Schnupperstunde beschließt der Gruppe beizutreten, benötigen wir noch ein paar
Informationen und eine schriftliche Anmeldebestätigung von dir. Das kannst du alles über folgenden Link erledigen:

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-27 23:35+0100\n"
"POT-Creation-Date: 2025-04-06 18:50+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -135,6 +135,104 @@ msgstr ""
msgid "You entered a wrong password."
msgstr "Das eingegebene Passwort ist falsch."
#: templates/admin/base.html
#, fuzzy
#| msgid "Welcome, "
msgid "Welcome,"
msgstr "Willkommen, "
#: templates/admin/base.html
#, fuzzy
#| msgid "View on site"
msgid "View site"
msgstr "Auf der Website anzeigen"
#: templates/admin/base.html
#, fuzzy
#| msgid "Authentication"
msgid "Documentation"
msgstr "Authentifizierung"
#: templates/admin/base.html
#, fuzzy
#| msgid "Password"
msgid "Change password"
msgstr "Passwort"
#: templates/admin/base.html
msgid "Log out"
msgstr ""
#: templates/admin/base.html
msgid "Home"
msgstr ""
#: templates/admin/base.html
msgid "back"
msgstr ""
#: templates/admin/base.html
#, fuzzy
#| msgid "Authentication"
msgid "Applications"
msgstr "Authentifizierung"
#: templates/admin/base.html
#, fuzzy
#| msgid "Generate SJR application"
msgid "Hide applications"
msgstr "SJR Antrag erstellen"
#: templates/admin/base.html
msgid "Show hidden"
msgstr ""
#: templates/admin/base.html
msgid "Add bookmark"
msgstr ""
#: templates/admin/base.html
msgid "Title"
msgstr ""
#: templates/admin/base.html
msgid "URL"
msgstr ""
#: templates/admin/base.html
msgid "Delete bookmark"
msgstr ""
#: templates/admin/base.html
#, fuzzy
#| msgid ""
#| "Are you sure you want to delete the %(object_name)s "
#| "\"%(escaped_object)s\"?"
msgid "Are you sure want to delete this bookmark?"
msgstr ""
"Bist du sicher, dass du %(object_name)s \"%(escaped_object)s\" und alle "
"davon abhängigen Objekte löschen möchtest? "
#: templates/admin/base.html
msgid "bookmarks"
msgstr ""
#: templates/admin/base.html
msgid "Remove"
msgstr ""
#: templates/admin/base.html
msgid "Search"
msgstr ""
#: templates/admin/base.html
msgid "Application page"
msgstr ""
#: templates/admin/base.html
msgid "current theme"
msgstr ""
#: templates/admin/delete_confirmation.html
#, python-format
msgid ""

@ -76,6 +76,10 @@ def get_invitation_reject_link(key):
return prepend_base_url("/members/waitinglist/invitation/reject?key={}".format(key))
def get_invitation_confirm_link(key):
return prepend_base_url("/members/waitinglist/invitation/confirm?key={}".format(key))
def get_wait_confirmation_link(waiter):
key = waiter.generate_wait_confirmation_key()
return prepend_base_url("/members/waitinglist/confirm?key={}".format(key))

@ -813,6 +813,15 @@ msgstr "%(waiter)s hat die Warteliste verlassen"
msgid "Group invitation rejected by %(waiter)s"
msgstr "Einladung zur Schnupperstunde von %(waiter)s abgelehnt"
#: members/models.py
#, python-format
msgid "Group invitation confirmed by %(waiter)s"
msgstr "Teilnahme an Schnupperstunde von %(waiter)s bestätigt"
#: members/models.py
msgid "Trial group meeting confirmed"
msgstr "Teilnahme an Schnupperstunde bestätigt"
#: members/models.py
msgid "Do you want to tell us something else?"
msgstr "Möchtest du uns noch etwas mitteilen?"
@ -1691,6 +1700,60 @@ msgstr "Fähigkeitsniveau"
msgid "Save and confirm registration"
msgstr "Speichern und Registrierung bestätigen"
#: members/templates/members/confirm_invalid.html
msgid "Confirm invitation"
msgstr "Teilnahme bestätigen"
#: members/templates/members/confirm_invalid.html
#: members/templates/members/reject_invalid.html members/tests.py
msgid "This invitation is invalid or expired."
msgstr "Diese Einladung ist ungültig oder abgelaufen."
#: members/templates/members/confirm_invitation.html
msgid "Confirm trial group meeting invitation"
msgstr "Teilnahme bestätigen"
#: members/templates/members/confirm_invitation.html
#, python-format
msgid "You were invited to a trial group meeting of the group %(groupname)s."
msgstr ""
"Du wurdest zu einer Schnupperstunde in der Gruppe %(groupname)s eingeladen."
#: members/templates/members/confirm_invitation.html
msgid ""
"Do you want to take part in the trial group meeting? If yes, please confirm "
"your attendance by clicking on the following button."
msgstr ""
"Möchtest du an der Schnupperstunde teilnehmen? Falls ja, bitte bestätige "
"deine Teilnahme durch das Betätigen des folgenden Knopfes."
#: members/templates/members/confirm_invitation.html
msgid "Confirm trial group meeting"
msgstr "Teilnahme bestätigen"
#: members/templates/members/confirm_success.html
msgid "Invitation confirmed"
msgstr "Teilnahme bestätigt"
#: members/templates/members/confirm_success.html
#, python-format
msgid ""
"You successfully confirmed the invitation to the trial group meeting of the "
"group %(groupname)s."
msgstr ""
"Deine Teilnahme an der Schnupperstunde der Gruppe %(groupname)s wurde "
"erfolgreich bestätigt."
#: members/templates/members/confirm_success.html
msgid ""
"We have informed the group leaders about your confirmation. If for some "
"reason you can not make it,\n"
"please contact the group leaders at"
msgstr ""
"Wir haben die Jugendleiter*innen der Jugendgruppe über deine Bestätigung "
"informiert. Falls du doch nicht zur Schnupperstunde kommen kannst, "
"informiere bitte die Jugendleiter*innen unter"
#: members/templates/members/echo.html
#: members/templates/members/echo_failed.html
#: members/templates/members/echo_password.html
@ -1978,10 +2041,6 @@ msgstr ""
msgid "Reject invitation"
msgstr "Einladung ablehnen"
#: members/templates/members/reject_invalid.html members/tests.py
msgid "This invitation is invalid or expired."
msgstr "Diese Einladung ist ungültig oder abgelaufen."
#: members/templates/members/reject_invitation.html
#, python-format
msgid ""
@ -2276,14 +2335,6 @@ msgstr "Ungültige Notfallkontakte"
#~ msgid "Good conduct certificate presentation needed"
#~ msgstr "Vorlage Führungszeugnis notwendig"
#, python-format
#~ msgid ""
#~ "Do you want to reject the invitation to a trial group meeting of the\n"
#~ "group %(invitation.group.name)s?"
#~ msgstr ""
#~ "Möchtest du die Einladung zur Schnupperstunde bei der Gruppe "
#~ "%(invitation.group.name)s wirklich ablehnen?"
#~ msgid "Yes, reject invitation"
#~ msgstr "Ja, Einladung ablehnen."

@ -19,7 +19,8 @@ from utils import RestrictedFileField, normalize_name
import os
from mailer.mailutils import send as send_mail, get_mail_confirmation_link,\
prepend_base_url, get_registration_link, get_wait_confirmation_link,\
get_invitation_reject_link, get_invite_as_user_key, get_leave_waitinglist_link
get_invitation_reject_link, get_invite_as_user_key, get_leave_waitinglist_link,\
get_invitation_confirm_link
from django.contrib.auth.models import User
from django.conf import settings
from django.core.validators import MinValueValidator
@ -110,6 +111,14 @@ class Group(models.Model):
# return if the group has all relevant time slot information filled
return self.weekday and self.start_time and self.end_time
def get_time_info(self):
if self.has_time_info():
return settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[self.weekday][1],
start_time=self.start_time.strftime('%H:%M'),
end_time=self.end_time.strftime('%H:%M'))
else:
return ""
def get_invitation_text_template(self):
"""The text template used to invite waiters to this group. This contains
placeholders for the name of the waiter and personalized links."""
@ -118,9 +127,7 @@ class Group(models.Model):
else:
group_link = ''
if self.has_time_info():
group_time = settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[self.weekday][1],
start_time=self.start_time.strftime('%H:%M'),
end_time=self.end_time.strftime('%H:%M'))
group_time = self.get_time_info()
else:
group_time = settings.GROUP_TIME_UNAVAILABLE_TEXT.format(contact_email=self.contact_email)
return settings.INVITE_TEXT.format(group_time=group_time,
@ -913,6 +920,21 @@ class InvitationToGroup(models.Model):
settings.DEFAULT_SENDING_MAIL,
recipient.email)
def send_confirm_notification_to(self, recipient):
send_mail(_('Group invitation confirmed by %(waiter)s') % {'waiter': self.waiter},
settings.GROUP_INVITATION_CONFIRMED_TEXT.format(name=recipient.prename,
waiter=self.waiter,
group=self.group),
settings.DEFAULT_SENDING_MAIL,
recipient.email)
def send_confirm_confirmation(self):
self.waiter.send_mail(_('Trial group meeting confirmed'),
settings.TRIAL_GROUP_MEETING_CONFIRMED_TEXT.format(name=self.waiter.prename,
group=self.group,
contact_email=self.group.contact_email,
timeinfo=self.group.get_time_info()))
def notify_left_waitinglist(self):
"""
Inform youth leaders of the group and the inviter that the waiter left the waitinglist,
@ -933,6 +955,18 @@ class InvitationToGroup(models.Model):
for jl in self.group.leiters.all():
self.send_reject_notification_to(jl)
def confirm(self):
"""Confirm this invitation. Informs the youth leaders of the group of the invitation."""
self.rejected = False
self.save()
# confirm the confirmation
self.send_confirm_confirmation()
# send notifications
if self.created_by:
self.send_confirm_notification_to(self.created_by)
for jl in self.group.leiters.all():
self.send_confirm_notification_to(jl)
class MemberWaitingList(Person):
"""A participant on the waiting list"""
@ -1064,7 +1098,8 @@ class MemberWaitingList(Person):
self.send_mail(_("Invitation to trial group meeting"),
text_template.format(name=self.prename,
link=get_registration_link(invitation.key),
invitation_reject_link=get_invitation_reject_link(invitation.key)),
invitation_reject_link=get_invitation_reject_link(invitation.key),
invitation_confirm_link=get_invitation_confirm_link(invitation.key)),
cc=group.contact_email.email)
def unregister(self):

@ -0,0 +1,15 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Confirm invitation" %}
{% endblock %}
{% block content %}
<h1>{% trans "Confirm invitation" %}</h1>
<p>{% trans "This invitation is invalid or expired." %}</p>
{% endblock %}

@ -0,0 +1,29 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Confirm trial group meeting invitation" %}
{% endblock %}
{% block content %}
<h1>{% trans "Confirm trial group meeting invitation" %}</h1>
<p>
{% blocktrans %}You were invited to a trial group meeting of the group {{ groupname }}.{% endblocktrans %}
{{ timeinfo }}
</p>
<p>
{% 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 %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="key" value="{{invitation.key}}">
<input type="submit" name="confirm_invitation"
value="{% trans "Confirm trial group meeting" %}"/>
</form>
{% endblock %}

@ -0,0 +1,23 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Invitation confirmed" %}
{% endblock %}
{% block content %}
<h1>{% trans "Invitation confirmed" %}</h1>
<p>
{% blocktrans %}You successfully confirmed the invitation to the trial group meeting of the group {{ groupname }}.{% endblocktrans %}
{{ timeinfo }}
</p>
<p>
{% 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 %}
<a href="mailto:{{ contact_email }}">{{ contact_email }}</a>.
</p>
{% endblock %}

@ -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'),
]

@ -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']

Loading…
Cancel
Save