From 8b932461b768a7108b8114e6a1d3dba2833bc610 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sat, 18 Jan 2025 20:21:20 +0100 Subject: [PATCH] finance/statement: add selection fields for allowance and subsidies In the statement tab on excursions, add two new fields to select to which youth leaders the allowance and subsidies should be paid. The fields are checked for validity based on the number of approved youth leaders. Also add a new field on excursions to allow for additional approved youth leaders. The new fields are not yet used in the statement confirmation process. --- .../finance/locale/de/LC_MESSAGES/django.po | 27 +++++++++- ...6_statement_add_allowance_to_subsidy_to.py | 26 ++++++++++ jdav_web/finance/models.py | 19 ++++--- jdav_web/members/admin.py | 52 ++++++++++++++++++- .../members/locale/de/LC_MESSAGES/django.po | 47 +++++++++++++++-- ...izeit_approved_extra_youth_leader_count.py | 18 +++++++ jdav_web/members/models.py | 16 ++++++ 7 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py create mode 100644 jdav_web/members/migrations/0033_freizeit_approved_extra_youth_leader_count.py diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po index 6c85b0f..8738751 100644 --- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-01 21:48+0100\n" +"POT-Creation-Date: 2025-01-18 20:19+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -157,6 +157,31 @@ msgstr "Erklärung" msgid "Associated excursion" msgstr "Zugehörige Ausfahrt" +#: finance/models.py +msgid "Pay allowance to" +msgstr "Aufwandsentschädigung auszahlen an" + +#: finance/models.py +msgid "" +"The youth leaders to which an allowance should be paid. The count must match " +"the number of permitted youth leaders." +msgstr "" +"Die Jugendleiter*innen an die eine Aufwandsentschädigung ausgezahlt werden " +"soll. Die Anzahl muss mit der Anzahl an zugelassenen Jugendleiter*innen " +"übereinstimmen. " + +#: finance/models.py +msgid "Pay subsidy to" +msgstr "Übernachtungs- und Fahrtkostenzuschüsse auszahlen an" + +#: finance/models.py +msgid "" +"The person that should receive the subsidy for night and travel costs. " +"Typically the person who paid for them." +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 "Price per night" msgstr "Preis pro Nacht" diff --git a/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py b/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py new file mode 100644 index 0000000..df2d124 --- /dev/null +++ b/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.1 on 2025-01-18 19:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0032_member_upload_registration_form_key'), + ('members', '0033_freizeit_approved_extra_youth_leader_count'), + ('finance', '0005_alter_bill_proof'), + ] + + operations = [ + migrations.AddField( + model_name='statement', + name='allowance_to', + field=models.ManyToManyField(help_text='The youth leaders to which an allowance should be paid. The count must match the number of permitted youth leaders.', related_name='receives_allowance_for_statements', to='members.Member', verbose_name='Pay allowance to'), + ), + migrations.AddField( + model_name='statement', + name='subsidy_to', + field=models.ForeignKey(help_text='The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='receives_subsidy_for_statements', to='members.member', verbose_name='Pay subsidy to'), + ), + ] diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py index d27b74f..ed437e7 100644 --- a/jdav_web/finance/models.py +++ b/jdav_web/finance/models.py @@ -5,6 +5,7 @@ from django.utils import timezone from .rules import is_creator, not_submitted, leads_excursion from members.rules import is_leader, statement_not_submitted +from django.core.exceptions import ValidationError from django.db import models from django.db.models import Sum from django.utils.translation import gettext_lazy as _ @@ -58,6 +59,15 @@ class Statement(CommonModel): null=True, on_delete=models.SET_NULL) + allowance_to = models.ManyToManyField(Member, verbose_name=_('Pay allowance to'), + related_name='receives_allowance_for_statements', + help_text=_('The youth leaders to which an allowance should be paid. The count must match the number of permitted youth leaders.')) + subsidy_to = models.ForeignKey(Member, verbose_name=_('Pay subsidy to'), + null=True, + on_delete=models.SET_NULL, + 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.')) + 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) @@ -307,15 +317,8 @@ class Statement(CommonModel): are refinanced though.""" if self.excursion is None: return 0 - - #raw_staff_count = self.excursion.jugendleiter.count() - participant_count = self.excursion.participant_count - if participant_count < 4: - return 0 - elif 4 <= participant_count <= 7: - return 2 else: - return 2 + math.ceil((participant_count - 7) / 7) + return self.excursion.approved_staff_count @property def total(self): diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 71c7257..6cb8164 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -836,13 +836,62 @@ class BillOnExcursionInline(CommonAdminInlineMixin, admin.TabularInline): } +class StatementOnListForm(forms.ModelForm): + """ + Form to edit a statement attached to an excursion. This is used in an inline on + the excursion admin. + """ + def __init__(self, *args, **kwargs): + excursion = kwargs.pop('parent_obj') + super(StatementOnListForm, self).__init__(*args, **kwargs) + # only allow youth leaders of this excursion to be selected as recipients + # of subsidies and allowance + self.fields['allowance_to'].queryset = excursion.jugendleiter.all() + self.fields['subsidy_to'].queryset = excursion.jugendleiter.all() + + class Meta: + model = Statement + fields = ['night_cost', 'allowance_to', 'subsidy_to'] + + def clean(self): + """Check if the `allowance_to` and `subsidy_to` fields are compatible with + the total number of approved youth leaders.""" + allowance_to = self.cleaned_data.get('allowance_to') + excursion = self.cleaned_data.get('excursion') + if allowance_to is None: + return + if allowance_to.count() > excursion.approved_staff_count: + raise ValidationError({ + 'allowance_to': _("This excursion only has up to %(approved_count)s approved youth leaders, but you listed %(entered_count)s." % {'approved_count': str(excursion.approved_staff_count), + 'entered_count': str(allowance_to.count())}), + }) + if allowance_to.count() < min(excursion.approved_staff_count, excursion.jugendleiter.count()): + raise ValidationError({ + 'allowance_to': _("This excursion has %(approved_count)s approved youth leaders, but you listed only %(entered_count)s." % {'approved_count': str(excursion.approved_staff_count), + 'entered_count': str(allowance_to.count())}) + }) + + class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): model = Statement 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'] + fields = ['night_cost', 'allowance_to', 'subsidy_to'] inlines = [BillOnExcursionInline] + form = StatementOnListForm + + def get_formset(self, request, obj=None, **kwargs): + BaseFormSet = kwargs.pop('formset', self.formset) + + class CustomFormSet(BaseFormSet): + def get_form_kwargs(self, index): + kwargs = super().get_form_kwargs(index) + kwargs['parent_obj'] = obj + return kwargs + + kwargs['formset'] = CustomFormSet + return super().get_formset(request, obj, **kwargs) class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline): @@ -943,6 +992,7 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): fieldsets = ( (None, { 'fields': ('name', 'place', 'destination', 'date', 'end', 'description', 'groups', 'jugendleiter', + 'approved_extra_youth_leader_count', 'tour_type', 'tour_approach', 'kilometers_traveled', 'activity', 'difficulty'), 'description': _('General information on your excursion. These are partly relevant for the amount of financial compensation (means of transport, travel distance, etc.).') }), diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index 9a00530..b43fbfb 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-01-01 21:39+0100\n" +"POT-Creation-Date: 2025-01-18 20:19+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -187,10 +187,18 @@ msgstr "Gruppe" msgid "Invitation text" msgstr "Einladungstext" +#: members/admin.py +msgid "Age" +msgstr "Alter" + #: members/admin.py msgid "Pending group invitation for group" msgstr "Ausstehende Gruppeneinladung für Gruppe" +#: members/admin.py members/models.py +msgid "age" +msgstr "Alter" + #: members/admin.py #, python-format msgid "Successfully asked %(name)s to confirm their waiting status." @@ -251,6 +259,24 @@ msgstr "Art der Tour" msgid "Means of transportation" msgstr "Verkehrsmittel" +#: members/admin.py +#, python-format +msgid "" +"This excursion only has up to %(approved_count)s approved youth leaders, but " +"you listed %(entered_count)s." +msgstr "" +"Diese Ausfahrt hat nur bis zu %(approved_count)s zugelassene " +"Jugendleiter*innen, aber du hast %(entered_count)s eingetragen." + +#: members/admin.py +#, python-format +msgid "" +"This excursion has %(approved_count)s approved youth leaders, but you listed " +"only %(entered_count)s." +msgstr "" +"Diese Ausfahrt hat %(approved_count)s zugelassene Jugendleiter*innen, aber " +"du hast nur %(entered_count)s eingetragen." + #: members/admin.py msgid "" "Please list here all expenses in relation with this excursion and upload " @@ -496,10 +522,6 @@ msgstr "Gender" msgid "comments" msgstr "Kommentare" -#: members/models.py -msgid "age" -msgstr "Alter" - #: members/models.py msgid "Alternative email confirmed" msgstr "Alternative E-Mail Adresse bestätigt" @@ -785,6 +807,21 @@ msgstr "Ende" msgid "Groups" msgstr "Gruppen" +#: members/models.py +msgid "Number of additional approved youth leaders" +msgstr "Anzahl zusätzlich genehmigter Jugendleiter*innen" + +#: members/models.py +msgid "" +"The number of approved youth leaders per excursion is determined by the " +"number of participants. In special circumstances, e.g. in case of a " +"technically demanding excursion, more youth leaders may be approved." +msgstr "" +"Die Anzahl der genehmigten Jugendleiter*innen pro Ausfahrt wird " +"grundsätzlich durch die Anzahl der Teilnehmer*innen festgelegt. In " +"besonderen Fällen, zum Beispiel bei einer fachlich herausfordernden " +"Ausfahrt, können zusätzliche Jugendleiter*innen genehmigt werden." + #: members/models.py msgid "Kilometers traveled" msgstr "Fahrstrecke in Kilometer" diff --git a/jdav_web/members/migrations/0033_freizeit_approved_extra_youth_leader_count.py b/jdav_web/members/migrations/0033_freizeit_approved_extra_youth_leader_count.py new file mode 100644 index 0000000..5d7d9c2 --- /dev/null +++ b/jdav_web/members/migrations/0033_freizeit_approved_extra_youth_leader_count.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2025-01-18 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0032_member_upload_registration_form_key'), + ] + + operations = [ + migrations.AddField( + model_name='freizeit', + name='approved_extra_youth_leader_count', + field=models.PositiveIntegerField(default=0, help_text='The number of approved youth leaders per excursion is determined by the number of participants. In special circumstances, e.g. in case of a technically demanding excursion, more youth leaders may be approved.', verbose_name='Number of additional approved youth leaders'), + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 4bef718..58adb57 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -1048,6 +1048,9 @@ class Freizeit(CommonModel): # comment = models.TextField(_('Comments'), default='', blank=True) groups = models.ManyToManyField(Group, verbose_name=_('Groups')) jugendleiter = models.ManyToManyField(Member) + approved_extra_youth_leader_count = models.PositiveIntegerField(verbose_name=_('Number of additional approved youth leaders'), + default=0, + help_text=_('The number of approved youth leaders per excursion is determined by the number of participants. In special circumstances, e.g. in case of a technically demanding excursion, more youth leaders may be approved.')) tour_type_choices = ((GEMEINSCHAFTS_TOUR, 'Gemeinschaftstour'), (FUEHRUNGS_TOUR, 'Führungstour'), (AUSBILDUNGS_TOUR, 'Ausbildung')) @@ -1134,6 +1137,19 @@ class Freizeit(CommonModel): jls = set(self.jugendleiter.distinct()) return len(ps - jls) + @property + def approved_staff_count(self): + """Number of approved youth leaders for this excursion. The base number is calculated + from the participant count. To this, the number of additional approved youth leaders is added.""" + participant_count = self.participant_count + if participant_count < 4: + base_count = 0 + elif 4 <= participant_count <= 7: + base_count = 2 + else: + base_count = 2 + math.ceil((participant_count - 7) / 7) + return base_count + self.approved_extra_youth_leader_count + @property def ljp_participant_count(self): ps = set(map(lambda x: x.member, self.membersonlist.distinct()))