diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py index 71e260c..f60cb8b 100644 --- a/jdav_web/jdav_web/settings/local.py +++ b/jdav_web/jdav_web/settings/local.py @@ -51,6 +51,14 @@ SEND_FROM_ASSOCIATION_EMAIL = get_var('misc', 'send_from_association_email', def # domain for association email and generated urls DOMAIN = get_var('misc', 'domain', default='example.org') +GROUP_CHECKLIST_N_WEEKS = get_var('misc', 'group_checklist_n_weeks', default=18) +GROUP_CHECKLIST_N_MEMBERS = get_var('misc', 'group_checklist_n_members', default=20) +GROUP_CHECKLIST_TEXT = get_var('misc', 'group_checklist_text', + default="""Anwesende Jugendleitende und Teilnehmende werden mit einem +Kreuz ($\\times$) markiert und die ausgefüllte Liste zum Anfang der Gruppenstunde an der Kasse +abgegeben. Zum Ende wird sie wieder abgeholt. Wenn die Punkte auf einer Karte fast aufgebraucht +sind, notiert die Kasse die verbliebenen Eintritte (3, 2, 1) unter dem Kreuz.""") + # finance ALLOWANCE_PER_DAY = get_var('finance', 'allowance_per_day', default=22) diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index 1adcf4e..7caacfe 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-04-06 19:10+0200\n" +"POT-Creation-Date: 2025-04-15 23:05+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -276,6 +276,10 @@ msgstr "Kostenübersicht" msgid "Generate group overview" msgstr "Gruppenübersicht erstellen" +#: templates/admin/members/group/change_list.html +msgid "Generate group checklist" +msgstr "Gruppencheckliste erstellen" + #: templates/admin/members/member/change_form_object_tools.html msgid "Invite as user" msgstr "Als Kompassbenutzer*in einladen" diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 9cf41bf..28c8edf 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -30,6 +30,7 @@ from django.shortcuts import render from django.core.exceptions import PermissionDenied, ValidationError from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf, render_docx from .excel import generate_group_overview, generate_ljp_vbk +from .models import WEEKDAYS from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin @@ -44,7 +45,7 @@ from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, K from finance.models import Statement, BillOnExcursionProxy from mailer.mailutils import send as send_mail, get_echo_link from django.conf import settings -from utils import get_member, RestrictedFileField +from utils import get_member, RestrictedFileField, mondays_until_nth from schwifty import IBAN from .pdf import media_path, media_dir @@ -195,7 +196,6 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): ('join_date', 'leave_date'), 'comments', 'legal_guardians', - 'dav_badge_no', 'active', 'echoed', 'user', ] @@ -213,8 +213,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): ), (_("Others"), { - 'fields': ['allergies', 'tetanus_vaccination', 'medication', 'photos_may_be_taken', - 'may_cancel_appointment_independently'] + 'fields': ['dav_badge_no', 'ticket_no', 'allergies', 'tetanus_vaccination', + 'medication', 'photos_may_be_taken','may_cancel_appointment_independently'] } ), (_("Organizational"), @@ -854,6 +854,8 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): def action_view(self, request): if "group_overview" in request.POST: return self.group_overview(request) + elif "group_checklist" in request.POST: + return self.group_checklist(request) def group_overview(self, request): @@ -867,6 +869,28 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): response = serve_media(filename=filename, content_type='application/xlsx') return response + + def group_checklist(self, request): + + if not request.user.has_perm('members.view_group'): + messages.error(request, + _("You are not allowed to create a group checklist.")) + return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) + + ensure_media_dir() + n_weeks = settings.GROUP_CHECKLIST_N_WEEKS + n_members = settings.GROUP_CHECKLIST_N_MEMBERS + + context = { + 'groups': self.model.objects.filter(show_website=True), + 'settings': settings, + 'week_range': range(n_weeks), + 'member_range': range(n_members), + 'dates': mondays_until_nth(n_weeks), + 'weekdays': [long for i, long in WEEKDAYS], + 'header_text': settings.GROUP_CHECKLIST_TEXT, + } + return render_tex(f"Gruppen-Checkliste", 'members/group_checklist.tex', context) class ActivityCategoryAdmin(admin.ModelAdmin): diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index 3344f9c..d231df7 100644 --- a/jdav_web/members/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/members/locale/de/LC_MESSAGES/django.po @@ -301,6 +301,11 @@ msgid "You are not allowed to create a group overview." msgstr "" "Du hast nicht die notwendigen Rechte um eine Gruppenübersicht zu erstellen." +#: members/admin.py +msgid "You are not allowed to create a group checklist." +msgstr "" +"Du hast nicht die notwendigen Rechte um eine Gruppencheckliste zu erstellen." + #: members/admin.py msgid "Difficulty" msgstr "Schwierigkeit" @@ -688,6 +693,10 @@ msgstr "Hat Freikarte für Kletterhalle" msgid "DAV badge number" msgstr "DAV Mitgliedsnummer" +#: members/models.py +msgid "entrance ticket number" +msgstr "Eintrittskarten Nummer" + #: members/models.py msgid "Knows how to swim" msgstr "Kann schwimmen" diff --git a/jdav_web/members/migrations/0042_member_ticket_no.py b/jdav_web/members/migrations/0042_member_ticket_no.py new file mode 100644 index 0000000..c1c28fb --- /dev/null +++ b/jdav_web/members/migrations/0042_member_ticket_no.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.20 on 2025-06-22 11:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0041_freizeit_crisis_intervention_list_sent_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='member', + name='ticket_no', + field=models.CharField(blank=True, default='', max_length=20, verbose_name='entrance ticket number'), + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 0d8fe64..660eac0 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -104,6 +104,11 @@ class Group(models.Model): class Meta: verbose_name = _('group') verbose_name_plural = _('groups') + + @property + def sorted_members(self): + """Returns the members of this group sorted by their last name.""" + return self.member_set.all().order_by('lastname') def has_time_info(self): # return if the group has all relevant time slot information filled @@ -318,6 +323,9 @@ class Member(Person): has_key = models.BooleanField(_('Has key'), default=False) has_free_ticket_gym = models.BooleanField(_('Has a free ticket for the climbing gym'), default=False) dav_badge_no = models.CharField(max_length=20, verbose_name=_('DAV badge number'), default='', blank=True) + + # use this to store a climbing gym customer or membership id, used to print on meeting checklists + ticket_no = models.CharField(max_length=20, verbose_name=_('entrance ticket number'), default='', blank=True) swimming_badge = models.BooleanField(verbose_name=_('Knows how to swim'), default=False) climbing_badge = models.CharField(max_length=100, verbose_name=_('Climbing badge'), default='', blank=True) alpine_experience = models.TextField(verbose_name=_('Alpine experience'), default='', blank=True) @@ -379,6 +387,11 @@ class Member(Person): def place(self): """Returning the whole place (plz + town)""" return "{0} {1}".format(self.plz, self.town) + + @property + def ticket_tag(self): + """Returning the ticket number stripped of strings and spaces""" + return "{" + ''.join(re.findall(r'\d', self.ticket_no)) + "}" @property def iban_valid(self): diff --git a/jdav_web/members/templates/members/group_checklist.tex b/jdav_web/members/templates/members/group_checklist.tex new file mode 100644 index 0000000..81ff72d --- /dev/null +++ b/jdav_web/members/templates/members/group_checklist.tex @@ -0,0 +1,65 @@ +{% extends "members/tex_base.tex" %} +{% load static common tex_extras %} + +{% block headline %}{% endblock %} +{% block contact %}{% endblock %} + +{% block extra-preamble %} +\usepackage{rotating} +\usepackage[code=Code39,X=.48mm,ratio=3.5,H=0.5cm]{makebarcode} +\geometry{reset,margin=1cm, bottom=1.5cm} +\renewcommand{\arraystretch}{1} +{% endblock %} + +{% block content %} +{% settings_value 'DEFAULT_STATIC_PATH' as static_root %} + +{% for group in groups %} +\picpos{2.5cm}{16cm}{-0.4cm}{% +{{ static_root }}/general/img/dav_logo_sektion.png% +} +% HEADLINE + +{\noindent\Large{Gruppenliste {{ group.name }} }}\\[1mm] +{% if group.has_time_info %} \noindent {{ weekdays|index:group.weekday|esc_all }}, {{ group.start_time }} - {{ group.end_time }} Uhr\\ {% endif %} +\noindent {{ header_text }} +\begin{table}[H] + \centering +%\begin{tabularx}{\textwidth}{lYY|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l} +\begin{tabularx}{\textwidth}{X{% for i in week_range %}|l{% endfor%}} +\toprule + \textbf{Name} {% for i in week_range %} + & \begin{sideways} {{ dates|index:i|add:group.weekday|date_vs }} \end{sideways} +{% endfor %} \\ + + {% for j in member_range %} + {% with m=group.sorted_members|index:j %} + {% with codelength=m.ticket_tag|length %} + \midrule + \begin{tabular}{@{}l} + {% if codelength > 2 %} + \barcode[ + X=\dimexpr 3.5mm / \numexpr {{ codelength }} \relax \relax + ]{{ m.ticket_tag }} + {% else %} + \rule{0pt}{5mm} + {% endif %} + \vspace{-0.8ex} \\ + {\small {{ j|plus:1 }} {% if m in group.leiters.all %}\textbf{JL}{% endif %} + {{ m.name|esc_all }} {% if codelength > 2 %} - {{ m.ticket_tag }}{% endif %} + \vspace{-3ex} } + \end{tabular} + + {% for i in week_range %} & {% endfor %}\\ + {% endwith %} + {% endwith %} + {% endfor %} + + \bottomrule +\end{tabularx} +\end{table} + +\clearpage +{% endfor %} + +{% endblock content %} diff --git a/jdav_web/members/templatetags/tex_extras.py b/jdav_web/members/templatetags/tex_extras.py index 02169e7..cdc03a3 100644 --- a/jdav_web/members/templatetags/tex_extras.py +++ b/jdav_web/members/templatetags/tex_extras.py @@ -1,5 +1,6 @@ from django import template from django.utils.safestring import mark_safe +from datetime import timedelta register = template.Library() @@ -14,6 +15,12 @@ def checked_if_true(name, value): def esc_all(val): return mark_safe(str(val).replace('_', '\\_').replace('&', '\\&').replace('%', '\\%')) +@register.filter +def index(sequence, position): + try: + return sequence[position] + except (IndexError, TypeError): + return '' @register.filter def datetime_short(date): @@ -24,7 +31,22 @@ def datetime_short(date): def date_short(date): return date.strftime('%d.%m.%y') +@register.filter +def date_vs(date): + return date.strftime('%d.%m.') @register.filter def time_short(date): return date.strftime('%H:%M') + +@register.filter +def add(date, days): + if days: + return date + timedelta(days=days) + return date + +@register.filter +def plus(num1, num2): + if num2: + return num1 + num2 + return num1 diff --git a/jdav_web/templates/admin/members/group/change_list.html b/jdav_web/templates/admin/members/group/change_list.html index e688e94..081adcd 100644 --- a/jdav_web/templates/admin/members/group/change_list.html +++ b/jdav_web/templates/admin/members/group/change_list.html @@ -7,6 +7,7 @@
{% csrf_token %} +
{{block.super}} diff --git a/jdav_web/utils.py b/jdav_web/utils.py index 129e495..021dd40 100644 --- a/jdav_web/utils.py +++ b/jdav_web/utils.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from django.db import models from django.utils import timezone from django.core.exceptions import ValidationError @@ -88,3 +88,11 @@ def coming_midnight(): return timezone.datetime(year=base.year, month=base.month, day=base.day, hour=0, minute=0, second=0, microsecond=0, tzinfo=base.tzinfo) + + +def mondays_until_nth(n): + """ Returns a list of dates for the next n Mondays, starting from the next Monday. + This functions aids in the generation of weekly schedules or reports.""" + today = datetime.today() + next_monday = today + timedelta(days=(7 - today.weekday()) % 7 or 7) + return [(next_monday + timedelta(weeks=i)).date() for i in range(n + 1)]