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.
pull/103/head
Christian Merten 11 months ago
parent ce466671f2
commit 8b932461b7
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -157,6 +157,31 @@ msgstr "Erklärung"
msgid "Associated excursion" msgid "Associated excursion"
msgstr "Zugehörige Ausfahrt" 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 #: finance/models.py
msgid "Price per night" msgid "Price per night"
msgstr "Preis pro Nacht" msgstr "Preis pro Nacht"

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

@ -5,6 +5,7 @@ from django.utils import timezone
from .rules import is_creator, not_submitted, leads_excursion from .rules import is_creator, not_submitted, leads_excursion
from members.rules import is_leader, statement_not_submitted from members.rules import is_leader, statement_not_submitted
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -58,6 +59,15 @@ class Statement(CommonModel):
null=True, null=True,
on_delete=models.SET_NULL) 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) 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) submitted = models.BooleanField(verbose_name=_('Submitted'), default=False)
@ -307,15 +317,8 @@ class Statement(CommonModel):
are refinanced though.""" are refinanced though."""
if self.excursion is None: if self.excursion is None:
return 0 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: else:
return 2 + math.ceil((participant_count - 7) / 7) return self.excursion.approved_staff_count
@property @property
def total(self): def total(self):

@ -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): class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = Statement model = Statement
extra = 1 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.).') 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 = [] sortable_options = []
fields = ['night_cost'] fields = ['night_cost', 'allowance_to', 'subsidy_to']
inlines = [BillOnExcursionInline] 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): class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline):
@ -943,6 +992,7 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('name', 'place', 'destination', 'date', 'end', 'description', 'groups', 'jugendleiter', 'fields': ('name', 'place', 'destination', 'date', 'end', 'description', 'groups', 'jugendleiter',
'approved_extra_youth_leader_count',
'tour_type', 'tour_approach', 'kilometers_traveled', 'activity', 'difficulty'), '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.).') 'description': _('General information on your excursion. These are partly relevant for the amount of financial compensation (means of transport, travel distance, etc.).')
}), }),

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -187,10 +187,18 @@ msgstr "Gruppe"
msgid "Invitation text" msgid "Invitation text"
msgstr "Einladungstext" msgstr "Einladungstext"
#: members/admin.py
msgid "Age"
msgstr "Alter"
#: members/admin.py #: members/admin.py
msgid "Pending group invitation for group" msgid "Pending group invitation for group"
msgstr "Ausstehende Gruppeneinladung für Gruppe" msgstr "Ausstehende Gruppeneinladung für Gruppe"
#: members/admin.py members/models.py
msgid "age"
msgstr "Alter"
#: members/admin.py #: members/admin.py
#, python-format #, python-format
msgid "Successfully asked %(name)s to confirm their waiting status." msgid "Successfully asked %(name)s to confirm their waiting status."
@ -251,6 +259,24 @@ msgstr "Art der Tour"
msgid "Means of transportation" msgid "Means of transportation"
msgstr "Verkehrsmittel" 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 #: members/admin.py
msgid "" msgid ""
"Please list here all expenses in relation with this excursion and upload " "Please list here all expenses in relation with this excursion and upload "
@ -496,10 +522,6 @@ msgstr "Gender"
msgid "comments" msgid "comments"
msgstr "Kommentare" msgstr "Kommentare"
#: members/models.py
msgid "age"
msgstr "Alter"
#: members/models.py #: members/models.py
msgid "Alternative email confirmed" msgid "Alternative email confirmed"
msgstr "Alternative E-Mail Adresse bestätigt" msgstr "Alternative E-Mail Adresse bestätigt"
@ -785,6 +807,21 @@ msgstr "Ende"
msgid "Groups" msgid "Groups"
msgstr "Gruppen" 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 #: members/models.py
msgid "Kilometers traveled" msgid "Kilometers traveled"
msgstr "Fahrstrecke in Kilometer" msgstr "Fahrstrecke in Kilometer"

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

@ -1048,6 +1048,9 @@ class Freizeit(CommonModel):
# comment = models.TextField(_('Comments'), default='', blank=True) # comment = models.TextField(_('Comments'), default='', blank=True)
groups = models.ManyToManyField(Group, verbose_name=_('Groups')) groups = models.ManyToManyField(Group, verbose_name=_('Groups'))
jugendleiter = models.ManyToManyField(Member) 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'), tour_type_choices = ((GEMEINSCHAFTS_TOUR, 'Gemeinschaftstour'),
(FUEHRUNGS_TOUR, 'Führungstour'), (FUEHRUNGS_TOUR, 'Führungstour'),
(AUSBILDUNGS_TOUR, 'Ausbildung')) (AUSBILDUNGS_TOUR, 'Ausbildung'))
@ -1134,6 +1137,19 @@ class Freizeit(CommonModel):
jls = set(self.jugendleiter.distinct()) jls = set(self.jugendleiter.distinct())
return len(ps - jls) 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 @property
def ljp_participant_count(self): def ljp_participant_count(self):
ps = set(map(lambda x: x.member, self.membersonlist.distinct())) ps = set(map(lambda x: x.member, self.membersonlist.distinct()))

Loading…
Cancel
Save