Merge remote-tracking branch 'origin/main' into MK/finance_tests

pull/155/head
mariusrklein 7 months ago
commit 2319984be4

@ -219,7 +219,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
messages.error(request,
_("%(name)s is not yet submitted.") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
if "transaction_execution_confirm" in request.POST:
if "transaction_execution_confirm" in request.POST or "transaction_execution_confirm_and_send" in request.POST:
res = statement.confirm(confirmer=get_member(request))
if not res:
# this should NOT happen!
@ -227,6 +227,9 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
_("An error occured while trying to confirm %(name)s. Please try again.") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name)))
if "transaction_execution_confirm_and_send" in request.POST:
statement.send_summary(cc=[request.user.member.email] if hasattr(request.user, 'member') else [])
messages.success(request, _("Successfully sent receipt to the office."))
messages.success(request,
_("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.")
% {'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-27 23:00+0200\n"
"POT-Creation-Date: 2025-05-03 19:06+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"
@ -64,6 +64,10 @@ msgstr ""
"Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es "
"erneut."
#: finance/admin.py
msgid "Successfully sent receipt to the office."
msgstr "Abrechnungsbeleg an die Geschäftsstelle gesendet."
#: finance/admin.py
#, python-format
msgid ""
@ -283,6 +287,10 @@ msgstr "Abrechnungen"
msgid "Statement: %(excursion)s"
msgstr "Abrechnung: %(excursion)s"
#: finance/models.py
msgid "Excursion %(excursion)s"
msgstr "Ausfahrt %(excursion)s"
#: finance/models.py
msgid "Ready to confirm"
msgstr "Bereit zur Abwicklung"
@ -310,6 +318,11 @@ msgstr "LJP-Zuschuss %(excu)s"
msgid "Total"
msgstr "Gesamtbetrag"
#: finance/models.py
#, python-format
msgid "Statement summary for %(title)s"
msgstr "Abrechnung für %(title)s"
#: finance/models.py
msgid "Statement in preparation"
msgstr "Abrechnung in Vorbereitung"
@ -433,8 +446,12 @@ msgid "I did execute the listed transactions."
msgstr "Ich habe die aufgeführten Überweisungen ausgeführt."
#: finance/templates/admin/confirmed_statement.html
msgid "Confirm"
msgstr "Bestätigen"
msgid "Confirm only"
msgstr "Nur bestätigen"
#: finance/templates/admin/confirmed_statement.html
msgid "Confirm and send receipt to office"
msgstr "Bestätigen und Beleg an die Geschäftsstelle senden"
#: finance/templates/admin/overview_submitted_statement.html
msgid "Overview"
@ -558,8 +575,8 @@ msgid ""
"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 "
"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

@ -15,6 +15,9 @@ import rules
from contrib.models import CommonModel
from contrib.rules import has_global_perm
from utils import cvt_to_decimal, RestrictedFileField
from members.pdf import render_tex_with_attachments
from mailer.mailutils import send as send_mail
from contrib.media import media_path
from schwifty import IBAN
import re
@ -121,6 +124,13 @@ class Statement(CommonModel):
else:
return self.short_description
@property
def title(self):
if self.excursion is not None:
return _('Excursion %(excursion)s') % {'excursion': str(self.excursion)}
else:
return self.short_description
def submit(self, submitter=None):
self.submitted = True
self.submitted_date = timezone.now()
@ -539,6 +549,23 @@ class Statement(CommonModel):
.order_by('short_description')\
.annotate(amount=Sum('amount'))
def send_summary(self, cc=None):
"""
Sends a summary of the statement to the central office of the association.
"""
excursion = self.excursion
context = dict(statement=self.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 self.bills_covered if bill.proof]
filename = render_tex_with_attachments(pdf_filename, 'finance/statement_summary.tex',
context, attachments, save_only=True)
send_mail(_('Statement summary for %(title)s') % { 'title': self.title },
settings.SEND_STATEMENT_SUMMARY.format(statement=self.title),
sender=settings.DEFAULT_SENDING_MAIL,
recipients=[settings.SEKTION_FINANCE_MAIL],
cc=cc,
attachments=[media_path(filename)])
class StatementUnSubmittedManager(models.Manager):
def get_queryset(self):

@ -110,6 +110,7 @@ links.forEach(link => {
<input type="checkbox" required>
{% blocktrans %}I did execute the listed transactions.{% endblocktrans %}
</p>
<input class="default confirm" type="submit" name="transaction_execution_confirm" value="{% translate 'Confirm' %}">
<input class="default confirm" type="submit" name="transaction_execution_confirm" value="{% translate 'Confirm only' %}">
<input class="default confirm" type="submit" name="transaction_execution_confirm_and_send" value="{% translate 'Confirm and send receipt to office' %}">
</form>
{% endblock %}

@ -247,3 +247,37 @@ Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBL
ADDRESS = get_text('address', default="""JDAV %(SEKTION)s
%(STREET)s
%(PLACE)s""" % { 'SEKTION': SEKTION, 'STREET': SEKTION_STREET, 'PLACE': SEKTION_TOWN })
NOTIFY_EXCURSION_PARTICIPANT_LIST = get_text('notify_excursion_participant_list', default="""Hallo {name},
deine Ausfahrt {excursion} steht kurz bevor. Damit die Sektion dich im Notfall gut unterstützen kann, benötigt
die Geschäftsstelle eine aktuelle Kriseninterventionsliste, das heißt eine Teilnehmendenliste der Ausfahrt.
Das Verschicken der Liste passiert automatisch zum Zeitpunkt: {sending_time}.
Das sind die aktuell in der Ausfahrt eingetragenen Teilnehmenden:
{participants}
Falls diese Liste nicht mehr aktuell ist, gehe bitte umgehend auf {excursion_link} und trage die Daten nach.
Viele Grüße
Dein KOMPASS""")
SEND_EXCURSION_CRISIS_LIST = get_text('send_excursion_crisis_list', default="""Hallo zusammen,
vom {excursion_start} bis {excursion_end} findet die Ausfahrt {excursion} der Jugend statt. Die
Ausfahrt wird geleitet von {leaders}.
Im Anhang findet ihr die Kriseninterventionsliste.
Viele Grüße
Euer KOMPASS""")
SEND_STATEMENT_SUMMARY = get_text('send_statement_summary', default="""Hallo zusammen,
anbei findet ihr die Abrechnung inklusive Belege für {statement}. Die Überweisungen
wurden wie beschrieben ausgeführt.
Viele Grüße
Euer KOMPASS""")

@ -9,6 +9,7 @@ SEKTION_CONTACT_MAIL = get_var('section', 'contact_mail', default='info@example.
SEKTION_BOARD_MAIL = get_var('section', 'board_mail', default=SEKTION_CONTACT_MAIL)
SEKTION_CRISIS_INTERVENTION_MAIL = get_var('section', 'crisis_intervention_mail',
default=SEKTION_BOARD_MAIL)
SEKTION_FINANCE_MAIL = get_var('section', 'finance_mail', default=SEKTION_CONTACT_MAIL)
SEKTION_IBAN = get_var('section', 'iban', default='Foo 123')
SEKTION_ACCOUNT_HOLDER = get_var('section', 'account_holder',
default='Foo')

@ -0,0 +1,227 @@
from .models import *
import re
import csv
def import_from_csv(path, omit_groupless=True):
with open(path, encoding='ISO-8859-1') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
rows = list(reader)
def transform_field(key, value):
new_key = CLUBDESK_TO_KOMPASS[key]
if isinstance(new_key, str):
return (new_key, value)
else:
return (new_key[0], new_key[1](value))
def transform_row(row):
kwargs = dict([ transform_field(k, v) for k, v in row.items() if k in CLUBDESK_TO_KOMPASS ])
kwargs_filtered = { k : v for k, v in kwargs.items() if k not in ['group', 'last_training', 'has_fundamental_training', 'special_training', 'phone_number_private', 'phone_number_parents'] }
if not kwargs['group'] and omit_groupless:
# if member does not have a group, skip them
return
mem = Member(**kwargs_filtered)
mem.save()
mem.group.set([group for group, is_jl in kwargs['group']])
for group, is_jl in kwargs['group']:
if is_jl:
group.leiters.add(mem)
if kwargs['has_fundamental_training']:
try:
ga_cat = TrainingCategory.objects.get(name='Grundausbildung')
except TrainingCategory.DoesNotExist:
ga_cat = TrainingCategory(name='Grundausbildung', permission_needed=True)
ga_cat.save()
ga_training = MemberTraining(member=mem, title='Grundausbildung', date=None, category=ga_cat,
participated=True, passed=True)
ga_training.save()
if kwargs['last_training'] is not None:
try:
cat = TrainingCategory.objects.get(name='Fortbildung')
except TrainingCategory.DoesNotExist:
cat = TrainingCategory(name='Fortbildung', permission_needed=False)
cat.save()
training = MemberTraining(member=mem, title='Unbekannt', date=kwargs['last_training'], category=cat,
participated=True, passed=True)
training.save()
if kwargs['special_training'] != '':
try:
cat = TrainingCategory.objects.get(name='Sonstiges')
except TrainingCategory.DoesNotExist:
cat = TrainingCategory(name='Sonstiges', permission_needed=False)
cat.save()
training = MemberTraining(member=mem, title=kwargs['special_training'], date=None, category=cat,
participated=True, passed=True)
training.save()
if kwargs['phone_number_private'] != '':
prefix = '\n' if mem.comments else ''
mem.comments += prefix + 'Telefon (Privat): ' + kwargs['phone_number_private']
mem.save()
if kwargs['phone_number_parents'] != '':
prefix = '\n' if mem.comments else ''
mem.comments += prefix + 'Telefon (Eltern): ' + kwargs['phone_number_parents']
mem.save()
for row in rows:
transform_row(row)
def parse_group(value):
groups_raw = re.split(',', value)
# need to determine if member is youth leader
roles = set()
def extract_group_name_and_role(raw):
obj = re.search('^(.*?)(?: \((.*)\))?$', raw)
is_jl = False
if obj.group(2) is not None:
roles.add(obj.group(2).strip())
if obj.group(2) == 'Jugendleiter*in':
is_jl = True
return (obj.group(1).strip(), is_jl)
group_names = [extract_group_name_and_role(raw) for raw in groups_raw if raw != '']
if "Jugendleiter*in" in roles:
group_names.append(('Jugendleiter', False))
groups = []
for group_name, is_jl in group_names:
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
group = Group(name=group_name)
group.save()
groups.append((group, is_jl))
return groups
def parse_date(value):
if value == '':
return None
return datetime.strptime(value, '%d.%m.%Y').date()
def parse_datetime(value):
tz = pytz.timezone('Europe/Berlin')
if value == '':
return timezone.now()
return tz.localize(datetime.strptime(value, '%d.%m.%Y %H:%M:%S'))
def parse_status(value):
return value != "Passivmitglied"
def parse_boolean(value):
return value.lower() == "ja"
def parse_nullable_boolean(value):
if value == '':
return None
else:
return value.lower() == "ja"
def parse_gender(value):
if value == 'männlich':
return MALE
elif value == 'weiblich':
return FEMALE
else:
return DIVERSE
def parse_can_swim(value):
return True if len(value) > 0 else False
CLUBDESK_TO_KOMPASS = {
'Nachname': 'lastname',
'Vorname': 'prename',
'Adresse': 'street',
'PLZ': 'plz',
'Ort': 'town',
'Telefon Privat': 'phone_number_private',
'Telefon Mobil': 'phone_number',
'Adress-Zusatz': 'address_extra',
'Land': 'country',
'E-Mail': 'email',
'E-Mail Alternativ': 'alternative_email',
'Status': ('active', parse_status),
'Eintritt': ('join_date', parse_date),
'Austritt': ('leave_date', parse_date),
'Geburtsdatum': ('birth_date', parse_date),
'Geburtstag': ('birth_date', parse_date),
'Geschlecht': ('gender', parse_gender),
'Bemerkungen': 'comments',
'IBAN': 'iban',
'Vorlage Führungszeugnis': ('good_conduct_certificate_presented_date', parse_date),
'Letzte Fortbildung': ('last_training', parse_date),
'Grundausbildung': ('has_fundamental_training', parse_boolean),
'Besondere Ausbildung': 'special_training',
'[Gruppen]' : ('group', parse_group),
'Schlüssel': ('has_key', parse_boolean),
'Freikarte': ('has_free_ticket_gym', parse_boolean),
'DAV Ausweis Nr.': 'dav_badge_no',
'Schwimmabzeichen': ('swimming_badge', parse_can_swim),
'Kletterschein': 'climbing_badge',
'Felserfahrung': 'alpine_experience',
'Allergien': 'allergies',
'Medikamente': 'medication',
'Tetanusimpfung': 'tetanus_vaccination',
'Fotoerlaubnis': ('photos_may_be_taken', parse_boolean),
'Erziehungsberechtigte': 'legal_guardians',
'Darf sich allein von der Gruppenstunde abmelden':
('may_cancel_appointment_independently', parse_nullable_boolean),
'Mobil Eltern': 'phone_number_parents',
'Sonstiges': 'application_text',
'Erhalten am': ('application_date', parse_datetime),
'Angeschrieben von': 'contacted_by',
'Angeschrieben von ': 'contacted_by',
}
def import_from_csv_waitinglist(path):
with open(path, encoding='ISO-8859-1') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
rows = list(reader)
def transform_field(key, value):
new_key = CLUBDESK_TO_KOMPASS[key]
if isinstance(new_key, str):
return (new_key, value)
else:
return (new_key[0], new_key[1](value))
def transform_field(key, value):
new_key = CLUBDESK_TO_KOMPASS[key]
if isinstance(new_key, str):
return (new_key, value)
else:
return (new_key[0], new_key[1](value))
def transform_row(row):
kwargs = dict([ transform_field(k, v) for k, v in row.items() if k in CLUBDESK_TO_KOMPASS ])
kwargs_filtered = { k : v for k, v in kwargs.items() if k in ['prename', 'lastname', 'email', 'birth_date', 'application_text', 'application_date'] }
mem = MemberWaitingList(gender=DIVERSE, **kwargs_filtered)
mem.save()
if kwargs['contacted_by']:
group_name = kwargs['contacted_by']
try:
group = Group.objects.get(name=group_name)
invitation = InvitationToGroup(group=group, waiter=mem)
invitation.save()
except Group.DoesNotExist:
pass
for row in rows:
transform_row(row)

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-27 23:00+0200\n"
"POT-Creation-Date: 2025-05-03 18:06+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"
@ -469,6 +469,15 @@ msgstr ""
msgid "Finance overview"
msgstr "Kostenübersicht"
#: members/admin.py
msgid "Inform youth leaders about sending of crisis intervention list."
msgstr ""
"Informiere Jugendleiter:innen über Versand der Kriseninterventionsliste."
#: members/admin.py
msgid "Send crisis intervention list."
msgstr "Kriseninterventionsliste verschicken"
#: members/apps.py
msgid "member administration"
msgstr "Teilnehmer*innenverwaltung"
@ -993,6 +1002,15 @@ msgstr "Ausfahrt"
msgid "Excursions"
msgstr "Ausfahrten"
#: members/models.py
msgid "Crisis intervention list for %(excursion)s from %(start)s to %(end)s"
msgstr "Kriseninterventionsliste für %(excursion)s vom %(start)s bis %(end)s"
#: members/models.py
#, python-format
msgid "Participant list for %(excursion)s from %(start)s to %(end)s"
msgstr "Teilnehmendenliste für %(excursion)s vom %(start)s bis %(end)s"
#: members/models.py
msgid "Title"
msgstr "Titel"
@ -1392,9 +1410,9 @@ msgid ""
"%(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 "
"ä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

@ -0,0 +1,23 @@
# Generated by Django 4.2.20 on 2025-05-03 15:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0040_invitationtogroup_created_by'),
]
operations = [
migrations.AddField(
model_name='freizeit',
name='crisis_intervention_list_sent',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='freizeit',
name='notification_crisis_intervention_list_sent',
field=models.BooleanField(default=False),
),
]

@ -4,7 +4,6 @@ import math
import pytz
import unicodedata
import re
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
@ -26,10 +25,12 @@ from django.conf import settings
from django.core.validators import MinValueValidator
from .rules import may_view, may_change, may_delete, is_own_training, is_oneself, is_leader, is_leader_of_excursion
from .pdf import render_tex
import rules
from contrib.models import CommonModel
from contrib.media import media_path
from contrib.rules import memberize_user, has_global_perm
from utils import cvt_to_decimal
from utils import cvt_to_decimal, coming_midnight
from dateutil.relativedelta import relativedelta
from schwifty import IBAN
@ -1217,6 +1218,10 @@ class Freizeit(CommonModel):
approval_comments = models.TextField(verbose_name=_('Approval comments'),
blank=True, default='')
# automatic sending of crisis intervention list
crisis_intervention_list_sent = models.BooleanField(default=False)
notification_crisis_intervention_list_sent = models.BooleanField(default=False)
def __str__(self):
"""String represenation"""
return self.name
@ -1345,18 +1350,18 @@ 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 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
@ -1616,6 +1621,61 @@ class Freizeit(CommonModel):
queryset = queryset.filter(Q(groups__in=groups) | Q(jugendleiter__pk=member.pk)).distinct()
return queryset
def send_crisis_intervention_list(self, sending_time=None):
"""
Send the crisis intervention list to the crisis invervention email, the
responsible and the youth leaders of this excursion.
"""
context = dict(memberlist=self, settings=settings)
start_date= timezone.localtime(self.date).strftime('%d.%m.%Y')
filename = render_tex(f"{self.code}_{self.name}_Krisenliste",
'members/crisis_intervention_list.tex', context,
date=self.date, save_only=True)
leaders = ", ".join([yl.name for yl in self.jugendleiter.all()])
start_date = timezone.localtime(self.date).strftime('%d.%m.%Y')
end_date = timezone.localtime(self.end).strftime('%d.%m.%Y')
# create email with attachment
send_mail(_('Crisis intervention list for %(excursion)s from %(start)s to %(end)s') %\
{ 'excursion': self.name,
'start': start_date,
'end': end_date },
settings.SEND_EXCURSION_CRISIS_LIST.format(excursion=self.name, leaders=leaders,
excursion_start=start_date,
excursion_end=end_date),
sender=settings.DEFAULT_SENDING_MAIL,
recipients=[settings.SEKTION_CRISIS_INTERVENTION_MAIL],
cc=[settings.RESPONSIBLE_MAIL] + [yl.email for yl in self.jugendleiter.all()],
attachments=[media_path(filename)])
self.crisis_intervention_list_sent = True
self.save()
def notify_leaders_crisis_intervention_list(self, sending_time=None):
"""
Send an email to the youth leaders of this excursion with a list of currently
registered participants and a heads-up that the crisis intervention list
will be automatically sent on the night of this day.
"""
participants = "\n".join([f"- {p.member.name}" for p in self.membersonlist.all()])
if not sending_time:
sending_time = coming_midnight().strftime("%d.%m.%y %H:%M")
elif not isinstance(sending_time, str):
sending_time = sending_time.strftime("%d.%m.%y %H:%M")
start_date = timezone.localtime(self.date).strftime('%d.%m.%Y')
end_date = timezone.localtime(self.end).strftime('%d.%m.%Y')
excursion_link = prepend_base_url(self.get_absolute_url())
for yl in self.jugendleiter.all():
yl.send_mail(_('Participant list for %(excursion)s from %(start)s to %(end)s') %\
{ 'excursion': self.name,
'start': start_date,
'end': end_date },
settings.NOTIFY_EXCURSION_PARTICIPANT_LIST.format(name=yl.prename,
excursion=self.name,
participants=participants,
sending_time=sending_time,
excursion_link=excursion_link))
self.notification_crisis_intervention_list_sent = True
self.save()
class MemberNoteList(models.Model):
"""
@ -1961,7 +2021,7 @@ class MemberTraining(CommonModel):
'image/jpeg',
'image/png',
'image/gif'])
class Meta(CommonModel.Meta):
verbose_name = _('Training')
verbose_name_plural = _('Trainings')
@ -1972,227 +2032,3 @@ class MemberTraining(CommonModel):
'change_obj': is_oneself | has_global_perm('members.change_global_membertraining'),
'delete_obj': is_oneself | has_global_perm('members.delete_global_membertraining'),
}
def import_from_csv(path, omit_groupless=True):
with open(path, encoding='ISO-8859-1') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
rows = list(reader)
def transform_field(key, value):
new_key = CLUBDESK_TO_KOMPASS[key]
if isinstance(new_key, str):
return (new_key, value)
else:
return (new_key[0], new_key[1](value))
def transform_row(row):
kwargs = dict([ transform_field(k, v) for k, v in row.items() if k in CLUBDESK_TO_KOMPASS ])
kwargs_filtered = { k : v for k, v in kwargs.items() if k not in ['group', 'last_training', 'has_fundamental_training', 'special_training', 'phone_number_private', 'phone_number_parents'] }
if not kwargs['group'] and omit_groupless:
# if member does not have a group, skip them
return
mem = Member(**kwargs_filtered)
mem.save()
mem.group.set([group for group, is_jl in kwargs['group']])
for group, is_jl in kwargs['group']:
if is_jl:
group.leiters.add(mem)
if kwargs['has_fundamental_training']:
try:
ga_cat = TrainingCategory.objects.get(name='Grundausbildung')
except TrainingCategory.DoesNotExist:
ga_cat = TrainingCategory(name='Grundausbildung', permission_needed=True)
ga_cat.save()
ga_training = MemberTraining(member=mem, title='Grundausbildung', date=None, category=ga_cat,
participated=True, passed=True)
ga_training.save()
if kwargs['last_training'] is not None:
try:
cat = TrainingCategory.objects.get(name='Fortbildung')
except TrainingCategory.DoesNotExist:
cat = TrainingCategory(name='Fortbildung', permission_needed=False)
cat.save()
training = MemberTraining(member=mem, title='Unbekannt', date=kwargs['last_training'], category=cat,
participated=True, passed=True)
training.save()
if kwargs['special_training'] != '':
try:
cat = TrainingCategory.objects.get(name='Sonstiges')
except TrainingCategory.DoesNotExist:
cat = TrainingCategory(name='Sonstiges', permission_needed=False)
cat.save()
training = MemberTraining(member=mem, title=kwargs['special_training'], date=None, category=cat,
participated=True, passed=True)
training.save()
if kwargs['phone_number_private'] != '':
prefix = '\n' if mem.comments else ''
mem.comments += prefix + 'Telefon (Privat): ' + kwargs['phone_number_private']
mem.save()
if kwargs['phone_number_parents'] != '':
prefix = '\n' if mem.comments else ''
mem.comments += prefix + 'Telefon (Eltern): ' + kwargs['phone_number_parents']
mem.save()
for row in rows:
transform_row(row)
def parse_group(value):
groups_raw = re.split(',', value)
# need to determine if member is youth leader
roles = set()
def extract_group_name_and_role(raw):
obj = re.search('^(.*?)(?: \((.*)\))?$', raw)
is_jl = False
if obj.group(2) is not None:
roles.add(obj.group(2).strip())
if obj.group(2) == 'Jugendleiter*in':
is_jl = True
return (obj.group(1).strip(), is_jl)
group_names = [extract_group_name_and_role(raw) for raw in groups_raw if raw != '']
if "Jugendleiter*in" in roles:
group_names.append(('Jugendleiter', False))
groups = []
for group_name, is_jl in group_names:
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
group = Group(name=group_name)
group.save()
groups.append((group, is_jl))
return groups
def parse_date(value):
if value == '':
return None
return datetime.strptime(value, '%d.%m.%Y').date()
def parse_datetime(value):
tz = pytz.timezone('Europe/Berlin')
if value == '':
return timezone.now()
return tz.localize(datetime.strptime(value, '%d.%m.%Y %H:%M:%S'))
def parse_status(value):
return value != "Passivmitglied"
def parse_boolean(value):
return value.lower() == "ja"
def parse_nullable_boolean(value):
if value == '':
return None
else:
return value.lower() == "ja"
def parse_gender(value):
if value == 'männlich':
return MALE
elif value == 'weiblich':
return FEMALE
else:
return DIVERSE
def parse_can_swim(value):
return True if len(value) > 0 else False
CLUBDESK_TO_KOMPASS = {
'Nachname': 'lastname',
'Vorname': 'prename',
'Adresse': 'street',
'PLZ': 'plz',
'Ort': 'town',
'Telefon Privat': 'phone_number_private',
'Telefon Mobil': 'phone_number',
'Adress-Zusatz': 'address_extra',
'Land': 'country',
'E-Mail': 'email',
'E-Mail Alternativ': 'alternative_email',
'Status': ('active', parse_status),
'Eintritt': ('join_date', parse_date),
'Austritt': ('leave_date', parse_date),
'Geburtsdatum': ('birth_date', parse_date),
'Geburtstag': ('birth_date', parse_date),
'Geschlecht': ('gender', parse_gender),
'Bemerkungen': 'comments',
'IBAN': 'iban',
'Vorlage Führungszeugnis': ('good_conduct_certificate_presented_date', parse_date),
'Letzte Fortbildung': ('last_training', parse_date),
'Grundausbildung': ('has_fundamental_training', parse_boolean),
'Besondere Ausbildung': 'special_training',
'[Gruppen]' : ('group', parse_group),
'Schlüssel': ('has_key', parse_boolean),
'Freikarte': ('has_free_ticket_gym', parse_boolean),
'DAV Ausweis Nr.': 'dav_badge_no',
'Schwimmabzeichen': ('swimming_badge', parse_can_swim),
'Kletterschein': 'climbing_badge',
'Felserfahrung': 'alpine_experience',
'Allergien': 'allergies',
'Medikamente': 'medication',
'Tetanusimpfung': 'tetanus_vaccination',
'Fotoerlaubnis': ('photos_may_be_taken', parse_boolean),
'Erziehungsberechtigte': 'legal_guardians',
'Darf sich allein von der Gruppenstunde abmelden':
('may_cancel_appointment_independently', parse_nullable_boolean),
'Mobil Eltern': 'phone_number_parents',
'Sonstiges': 'application_text',
'Erhalten am': ('application_date', parse_datetime),
'Angeschrieben von': 'contacted_by',
'Angeschrieben von ': 'contacted_by',
}
def import_from_csv_waitinglist(path):
with open(path, encoding='ISO-8859-1') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
rows = list(reader)
def transform_field(key, value):
new_key = CLUBDESK_TO_KOMPASS[key]
if isinstance(new_key, str):
return (new_key, value)
else:
return (new_key[0], new_key[1](value))
def transform_field(key, value):
new_key = CLUBDESK_TO_KOMPASS[key]
if isinstance(new_key, str):
return (new_key, value)
else:
return (new_key[0], new_key[1](value))
def transform_row(row):
kwargs = dict([ transform_field(k, v) for k, v in row.items() if k in CLUBDESK_TO_KOMPASS ])
kwargs_filtered = { k : v for k, v in kwargs.items() if k in ['prename', 'lastname', 'email', 'birth_date', 'application_text', 'application_date'] }
mem = MemberWaitingList(gender=DIVERSE, **kwargs_filtered)
mem.save()
if kwargs['contacted_by']:
group_name = kwargs['contacted_by']
try:
group = Group.objects.get(name=group_name)
invitation = InvitationToGroup(group=group, waiter=mem)
invitation.save()
except Group.DoesNotExist:
pass
for row in rows:
transform_row(row)

@ -1,7 +1,7 @@
from celery import shared_task
from django.utils import timezone
from django.conf import settings
from .models import MemberWaitingList
from .models import MemberWaitingList, Freizeit
@shared_task
def ask_for_waiting_confirmation():
@ -18,3 +18,31 @@ def ask_for_waiting_confirmation():
waiter.ask_for_wait_confirmation()
no += 1
return no
@shared_task
def send_crisis_intervention_list():
"""
Send crisis intervention lists for all excursions that start on the current day and
that have not been sent yet.
"""
no = 0
for excursion in Freizeit.objects.filter(date__date=timezone.now().date(),
crisis_intervention_list_sent=False):
excursion.send_crisis_intervention_list()
no += 1
return no
@shared_task
def send_notification_crisis_intervention_list():
"""
Send crisis intervention list notifiactions for all excursions that start on the next
day and that have not been sent yet.
"""
no = 0
for excursion in Freizeit.objects.filter(date__date=timezone.now().date() + timezone.timedelta(days=1),
notification_crisis_intervention_list_sent=False):
excursion.notify_leaders_crisis_intervention_list()
no += 1
return no

@ -631,11 +631,20 @@ class MemberAdminTestCase(AdminTestCase):
class FreizeitTestCase(BasicMemberTestCase):
def setUp(self):
super().setUp()
# this excursion is used for the counting tests
self.ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=1,
date=timezone.localtime())
# this excursion is used in the other tests
self.ex2 = Freizeit.objects.create(name='Wild trip 2', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=1,
date=timezone.localtime())
self.ex2.jugendleiter.add(self.fritz)
self.ex2.save()
def _setup_test_sjr_application_numbers(self, n_yl, n_b27_local, n_b27_non_local):
for i in range(n_yl):
@ -749,6 +758,17 @@ class FreizeitTestCase(BasicMemberTestCase):
for i in range(10):
self._test_sjr_application_numbers(10, 10 - i, i)
def test_notify_leaders_crisis_intervention_list(self):
self.ex2.notification_crisis_intervention_list_sent = False
self.ex2.notify_leaders_crisis_intervention_list()
self.assertTrue(self.ex2.notification_crisis_intervention_list_sent)
self.ex2.notify_leaders_crisis_intervention_list(sending_time=timezone.now())
def test_send_crisis_intervention_list(self):
self.ex2.crisis_intervention_list_sent = False
self.ex2.send_crisis_intervention_list()
self.assertTrue(self.ex2.crisis_intervention_list_sent)
class PDFActionMixin:
def _test_pdf(self, name, pk, model='freizeit', invalid=False, username='superuser', post_data=None):

@ -1,5 +1,6 @@
from datetime import datetime
from django.db import models
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from decimal import Decimal, ROUND_HALF_DOWN
@ -80,3 +81,10 @@ def normalize_filename(filename, append_date=True, date=None):
filename = filename.replace(' ', '_').replace('&', '').replace('/', '_')
# drop umlauts, accents etc.
return unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode()
def coming_midnight():
base = timezone.now() + timezone.timedelta(days=1)
return timezone.datetime(year=base.year, month=base.month, day=base.day,
hour=0, minute=0, second=0, microsecond=0,
tzinfo=base.tzinfo)

Loading…
Cancel
Save