Merge branch 'main' into MK/sjr_select_invoice

pull/103/head
mariusrklein 11 months ago
commit a9bf6178fc

@ -206,6 +206,14 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
_("Some transactions have no ledger configured. Please fill in the gaps.")
% {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
elif res == Statement.INVALID_ALLOWANCE_TO:
messages.error(request,
_("The configured recipients for the allowance don't match the regulations. Please correct this on the excursion."))
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
elif res == Statement.INVALID_TOTAL:
messages.error(request,
_("The calculated total amount does not match the sum of all transactions. This is most likely a bug."))
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "reject" in request.POST:

@ -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 22:42+0100\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"
@ -74,6 +74,22 @@ msgid "Some transactions have no ledger configured. Please fill in the gaps."
msgstr ""
"Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach."
#: finance/admin.py
msgid ""
"The configured recipients for the allowance don't match the regulations. "
"Please correct this on the excursion."
msgstr ""
"Die ausgewählten Empfänger*innen der Aufwandsentschädigungen entsprechen "
"nicht den Regularien. Bitte korrigiere das in der Ausfahrt."
#: finance/admin.py
msgid ""
"The calculated total amount does not match the sum of all transactions. This "
"is most likely a bug."
msgstr ""
"Der berechnete Gesamtbetrag stimmt nicht mit der Summe aller Überweisungen "
"überein. Das ist höchstwahrscheinlich ein Fehler in der Implementierung."
#: finance/admin.py
#, python-format
msgid "Successfully rejected %(name)s. The requestor can reapply, when needed."
@ -157,6 +173,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"
@ -208,8 +249,13 @@ msgstr "Bereit zur Abwicklung"
#: finance/models.py
#, python-format
msgid "Compensation for %(excu)s"
msgstr "Entschädigung für %(excu)s"
msgid "Allowance for %(excu)s"
msgstr "Aufwandsentschädigung für %(excu)s"
#: finance/models.py
#, python-format
msgid "Night and travel costs for %(excu)s"
msgstr "Übernachtungs- und Fahrtkosten für %(excu)s"
#: finance/models.py finance/templates/admin/overview_submitted_statement.html
msgid "Total"
@ -361,8 +407,8 @@ msgstr "Ausfahrt"
#, python-format
msgid "This excursion featured %(staff_count)s youth leader(s), each costing"
msgstr ""
"Diese Ausfahrt hatte %(staff_count)s Jugendleiter*innen. Auf jede*n "
"entfallen die folgenden Kosten:"
"Diese Ausfahrt hatte %(staff_count)s genehmigte Jugendleiter*innen. Auf "
"jede*n entfallen die folgenden Kosten:"
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
@ -400,6 +446,22 @@ msgstr ""
"Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, "
"insgesamt also %(total_staff)s€."
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
msgid "The allowance of %(allowance_per_yl)s€ per person should be paid to:"
msgstr ""
"Die Aufwandsentschädigung von %(allowance_per_yl)s€ pro Person soll "
"ausgezahlt werden an:"
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
msgid ""
"The subsidies for night and transportation costs of %(total_subsidies)s€ "
"should be paid to:"
msgstr ""
"Die Zuschüsse für Übernachtungs- und Fahrtkosten von %(total_subsidies)s€ "
"sollen ausgezahlt werden an:"
#: finance/templates/admin/overview_submitted_statement.html
#, python-format
msgid "This results in a total amount of %(total)s€"

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

@ -0,0 +1,19 @@
# Generated by Django 4.0.1 on 2025-01-18 22:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0033_freizeit_approved_extra_youth_leader_count'),
('finance', '0006_statement_add_allowance_to_subsidy_to'),
]
operations = [
migrations.AlterField(
model_name='statement',
name='allowance_to',
field=models.ManyToManyField(blank=True, 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'),
),
]

@ -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 _
@ -46,7 +47,7 @@ class StatementManager(models.Manager):
class Statement(CommonModel):
MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, VALID = 0, 1, 2
MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, INVALID_ALLOWANCE_TO, INVALID_TOTAL, VALID = 0, 1, 2, 3, 4
short_description = models.CharField(verbose_name=_('Short description'),
max_length=30,
@ -58,6 +59,16 @@ 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',
blank=True,
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)
@ -113,7 +124,9 @@ class Statement(CommonModel):
needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered and b.paid_by]
if self.excursion is not None:
needed_paiments.extend([(yl, self.real_per_yl) for yl in self.excursion.jugendleiter.all()])
needed_paiments.extend([(yl, self.allowance_per_yl) for yl in self.allowance_to.all()])
if self.subsidy_to:
needed_paiments.append((self.subsidy_to, self.total_subsidies))
needed_paiments = sorted(needed_paiments, key=lambda p: p[0].pk)
target = dict(map(lambda p: (p[0], sum([x[1] for x in p[1]])), groupby(needed_paiments, lambda p: p[0])))
@ -147,10 +160,25 @@ class Statement(CommonModel):
def transactions_match_expenses(self):
return len(self.transaction_issues) == 0
def is_valid(self):
return self.ledgers_configured and self.transactions_match_expenses
is_valid.boolean = True
is_valid.short_description = _('Ready to confirm')
@property
def allowance_to_valid(self):
"""Checks if the configured `allowance_to` field matches the regulations."""
if self.allowance_to.count() != self.real_staff_count:
return False
if self.excursion is not None:
yls = self.excursion.jugendleiter.all()
for yl in self.allowance_to.all():
if yl not in yls:
return False
return True
@property
def total_valid(self):
"""Checks if the calculated total agrees with the total amount of all transactions."""
total_transactions = 0
for transaction in self.transaction_set.all():
total_transactions += transaction.amount
return self.total == total_transactions
@property
def validity(self):
@ -158,9 +186,18 @@ class Statement(CommonModel):
return Statement.NON_MATCHING_TRANSACTIONS
if not self.ledgers_configured:
return Statement.MISSING_LEDGER
if not self.allowance_to_valid:
return Statement.INVALID_ALLOWANCE_TO
if not self.total_valid:
return Statement.INVALID_TOTAL
else:
return Statement.VALID
def is_valid(self):
return self.validity == Statement.VALID
is_valid.boolean = True
is_valid.short_description = _('Ready to confirm')
def confirm(self, confirmer=None):
if not self.submitted:
return False
@ -193,9 +230,14 @@ class Statement(CommonModel):
if self.excursion is None:
return True
for yl in self.excursion.jugendleiter.all():
ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=yl, amount=self.real_per_yl, confirmed=False, reference=ref).save()
# allowance
for yl in self.allowance_to.all():
ref = _("Allowance for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=yl, amount=self.allowance_per_yl, confirmed=False, reference=ref).save()
# subsidies (i.e. night and transportation costs)
ref = _("Night and travel costs for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=self.subsidy_to, amount=self.total_subsidies, confirmed=False, reference=ref).save()
return True
def reduce_transactions(self):
@ -290,6 +332,14 @@ class Statement(CommonModel):
return cvt_to_decimal(self.total_staff / self.excursion.staff_count)
@property
def total_subsidies(self):
"""
The total amount of subsidies excluding the allowance, i.e. the transportation
and night costs per youth leader multiplied with the real number of youth leaders.
"""
return (self.transportation_per_yl + self.nights_per_yl) * self.real_staff_count
@property
def total_staff(self):
return self.total_per_yl * self.real_staff_count
@ -307,15 +357,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):
@ -323,7 +366,13 @@ class Statement(CommonModel):
@property
def total_theoretic(self):
return self.total_bills_theoretic + self.total_staff
"""
The theoretic total used in SJR and LJP applications. This is the sum of all
bills (ignoring whether they are paid by the association or not) plus the
total allowance. This does not include the subsidies for night and travel costs,
since they are expected to be included in the bills.
"""
return self.total_bills_theoretic + self.total_allowance
def total_pretty(self):
return "{}".format(self.total)
@ -350,6 +399,7 @@ class Statement(CommonModel):
'transportation_per_yl': self.transportation_per_yl,
'total_per_yl': self.total_per_yl,
'total_staff': self.total_staff,
'total_subsidies': self.total_subsidies,
}
return dict(context, **excursion_context)
else:

@ -73,6 +73,25 @@
{% blocktrans %}In total this is {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %}
</p>
<p>
{% blocktrans %}The allowance of {{ allowance_per_yl }}€ per person should be paid to:{% endblocktrans %}
<ul>
{% for member in statement.allowance_to.all %}
<li>
{{ member.name }}
</li>
{% endfor %}
</ul>
</p>
<p>
{% blocktrans %}The subsidies for night and transportation costs of {{ total_subsidies }}€ should be paid to:{% endblocktrans %}
<ul>
<li>
{{ statement.subsidy_to.name }}
</li>
</ul>
</p>
{% endif %}
<h2>{% trans "Total" %}</h2>

@ -683,7 +683,11 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
now = timezone.now()
age_expr = ExpressionWrapper(
Case(
When(birth_date__month__gte=now.month, birth_date__day__gt=now.day, then=now.year - F('birth_date__year') - 1),
# if the month of the birth date has not yet passed, subtract one year
When(birth_date__month__gt=now.month, then=now.year - F('birth_date__year') - 1),
# if it is the month of the birth date but the day has not yet passed, subtract one year
When(birth_date__month=now.month, birth_date__day__gt=now.day, then=now.year - F('birth_date__year') - 1),
# otherwise return the difference in years
default=now.year - F('birth_date__year'),
),
output_field=IntegerField()
@ -832,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):
@ -948,6 +1001,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.).')
}),
@ -1073,6 +1127,10 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
if not memberlist.statement:
messages.error(request, _("No statement found. Please add a statement and then retry."))
if "apply" in request.POST:
if not memberlist.statement.allowance_to_valid:
messages.error(request,
_("The configured recipients of the allowance don't match the regulations. Please correct this and try again."))
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(memberlist.pk,)))
memberlist.statement.submit(get_member(request))
messages.success(request,
_("Successfully submited statement. The finance department will notify you as soon as possible."))

@ -259,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 "
@ -380,6 +398,14 @@ msgstr ""
"Keine Abrechnung angelegt. Bitte lege eine Abrechnung and und versuche es "
"erneut."
#: members/admin.py
msgid ""
"The configured recipients of the allowance don't match the regulations. "
"Please correct this and try again."
msgstr ""
"Die ausgewählten Empfänger*innen der Aufwandsentschädigung stimmen nicht mit "
"den Richtlinien überein. Bitte korrigiere das und versuche es erneut. "
#: members/admin.py
msgid ""
"Successfully submited statement. The finance department will notify you as "
@ -797,6 +823,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"
@ -1098,11 +1139,31 @@ msgstr ""
#: members/templates/admin/freizeit_finance_overview.html
#, python-format
msgid ""
"In total these are contributions of %(total_per_yl)s€ times %(staff_count)s, "
"giving %(total_staff)s€."
"The allowance of %(allowance_per_yl)s€ per person is configured to be paid "
"to:"
msgstr ""
"Die Aufwandsentschädigung von %(allowance_per_yl)s€ pro Person wird "
"ausgezahlt an:"
#: members/templates/admin/freizeit_finance_overview.html
#, python-format
msgid ""
"The subsidies for night and transportation costs of %(total_subsidies)s€ is "
"configured to be paid to:"
msgstr ""
"Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, "
"insgesamt also %(total_staff)s€."
"Die Zuschüsse für Übernachtungs- und Fahrtkosten von %(total_subsidies)s€ "
"werden ausgezahlt an:"
#: members/templates/admin/freizeit_finance_overview.html
msgid ""
"Warning: The configured recipients of the allowance don't match the "
"regulations. This might be because the number of recipients is bigger then "
"the number of admissable youth leaders for this excursion."
msgstr ""
"Warnung: Die ausgewählten Empfänger*innen der Aufwandsentschädigung stimmen "
"nicht mit den Richtlinien überein. Das kann daran liegen, dass die Anzahl "
"der ausgewählten Empfänger*innen die Anzahl genehmigter Jugendleiter*innen "
"übersteigt."
#: members/templates/admin/freizeit_finance_overview.html
msgid "LJP contributions"
@ -1817,6 +1878,14 @@ msgstr "abgelaufen"
msgid "Invalid emergency contacts"
msgstr "Ungültige Notfallkontakte"
#, python-format
#~ msgid ""
#~ "In total these are contributions of %(total_per_yl)s€ times "
#~ "%(staff_count)s, giving %(total_staff)s€."
#~ msgstr ""
#~ "Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, "
#~ "insgesamt also %(total_staff)s€."
#~ msgid "Your registration succeeded."
#~ msgstr "Deine Registrierung war erfolgreich."

@ -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)
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()))

@ -77,8 +77,28 @@ cost plan!
</ul>
</p>
<p>
{% blocktrans %}In total these are contributions of {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %}
{% blocktrans %}The allowance of {{ allowance_per_yl }}€ per person is configured to be paid to:{% endblocktrans %}
<ul>
{% for member in memberlist.statement.allowance_to.all %}
<li>
{{ member.name }}
</li>
{% endfor %}
</ul>
</p>
<p>
{% blocktrans %}The subsidies for night and transportation costs of {{ total_subsidies }}€ is configured to be paid to:{% endblocktrans %}
<ul>
<li>
{{ memberlist.statement.subsidy_to.name }}
</li>
</ul>
</p>
{% if not memberlist.statement.allowance_to_valid %}
<p>
{% blocktrans %}Warning: The configured recipients of the allowance don't match the regulations. This might be because the number of recipients is bigger then the number of admissable youth leaders for this excursion.{% endblocktrans %}
</p>
{% endif %}
<h3>{% trans "LJP contributions" %}</h3>

@ -146,8 +146,6 @@
\textbf{Beschreibung} & \textbf{Betrag} \\
\midrule
Aufwandsentschädigung & {{ memberlist.statement.total_allowance }}\\
Fahrtkosten & {{ memberlist.statement.total_transportation }}\\
Übernachtungskosten & {{ memberlist.statement.total_nights }}\\
{% for bill in memberlist.statement.grouped_bills %}
{{ bill.short_description|esc_all }} & {{ bill.amount }}\\
{% endfor %}

@ -6,14 +6,15 @@ from django.test import TestCase, Client, RequestFactory
from django.utils import timezone, translation
from django.conf import settings
from django.urls import reverse
from unittest import skip
from unittest import skip, mock
from .models import Member, Group, PermissionMember, PermissionGroup, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE,\
MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact,\
MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact, MemberWaitingList,\
DIVERSE, MALE, FEMALE
from .admin import MemberWaitingListAdmin, MemberAdmin, FreizeitAdmin
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from .admin import FreizeitAdmin
import random
import datetime
def create_custom_user(username, groups, prename, lastname):
@ -183,12 +184,12 @@ class PDFTestCase(TestCase):
class AdminTestCase(TestCase):
def setUp(self, model):
def setUp(self, model, admin):
self.factory = RequestFactory()
self.model = model
if model is not None:
self.admin = FreizeitAdmin(model, AdminSite())
User.objects.create_superuser(
if model is not None and admin is not None:
self.admin = admin(model, AdminSite())
superuser = User.objects.create_superuser(
username='superuser', password='secret'
)
standard = create_custom_user('standard', ['Standard'], 'Paul', 'Wulter')
@ -237,7 +238,7 @@ class AdminTestCase(TestCase):
class PermissionTestCase(AdminTestCase):
def setUp(self):
super().setUp(model=None)
super().setUp(model=None, admin=None)
def test_standard_permissions(self):
u = User.objects.get(username='standard')
@ -258,7 +259,7 @@ class PermissionTestCase(AdminTestCase):
class MemberAdminTestCase(AdminTestCase):
def setUp(self):
super().setUp(model=Member)
super().setUp(model=Member, admin=MemberAdmin)
cool_kids = Group.objects.get(name='cool kids')
super_kids = Group.objects.get(name='super kids')
mega_kids = Group.objects.create(name='mega kids')
@ -384,7 +385,7 @@ class MemberAdminTestCase(AdminTestCase):
class FreizeitAdminTestCase(AdminTestCase):
def setUp(self):
super().setUp(model=Freizeit)
super().setUp(model=Freizeit, admin=FreizeitAdmin)
ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
@ -461,6 +462,32 @@ class FreizeitAdminTestCase(AdminTestCase):
self.assertQuerysetEqual(queryset, Member.objects.none())
class MemberWaitingListAdminTestCase(AdminTestCase):
def setUp(self):
super().setUp(model=MemberWaitingList, admin=MemberWaitingListAdmin)
for i in range(10):
day = random.randint(1, 28)
month = random.randint(1, 12)
year = random.randint(1900, timezone.now().year)
ex = MemberWaitingList.objects.create(prename='Peter {}'.format(i),
lastname='Puter',
birth_date=datetime.date(year, month, day),
email=settings.TEST_MAIL,
gender=FEMALE)
def test_age_eq_birth_date_delta(self):
u = User.objects.get(username='superuser')
url = reverse('admin:members_memberwaitinglist_changelist')
request = self.factory.get(url)
request.user = u
queryset = self.admin.get_queryset(request)
today = timezone.now().date()
for m in queryset:
self.assertEqual(m.birth_date_delta, m.age(),
msg='Queryset based age calculation differs from python based age calculation for birth date {birth_date} compared to {today}.'.format(birth_date=m.birth_date, today=today))
class MailConfirmationTestCase(BasicMemberTestCase):
def setUp(self):
super().setUp()

Loading…
Cancel
Save