feat(finance/tests): tests for new rules (#155)

Also makes some checks safe.

Reviewed-on: #155
Reviewed-by: Christian Merten <christian@merten.dev>
Co-authored-by: marius.klein <marius.klein@alpenverein-heidelberg.de>
Co-committed-by: marius.klein <marius.klein@alpenverein-heidelberg.de>
pull/174/head
marius.klein 5 months ago committed by mariusrklein
parent bf68ba92d0
commit e6ad20e9c9

@ -405,6 +405,8 @@ class Statement(CommonModel):
@property @property
def org_fee_payant(self): def org_fee_payant(self):
if self.total_org_fee == 0:
return None
return self.subsidy_to if self.subsidy_to else self.allowance_to.all()[0] return self.subsidy_to if self.subsidy_to else self.allowance_to.all()[0]
@property @property
@ -466,8 +468,11 @@ class Statement(CommonModel):
return cvt_to_decimal( return cvt_to_decimal(
min( min(
# if total costs are more than the max amount of the LJP contribution, we pay the max amount, reduced by taxes
(1-settings.LJP_TAX) * settings.LJP_CONTRIBUTION_PER_DAY * self.excursion.ljp_participant_count * self.excursion.ljp_duration, (1-settings.LJP_TAX) * settings.LJP_CONTRIBUTION_PER_DAY * self.excursion.ljp_participant_count * self.excursion.ljp_duration,
# if the total costs are less than the max amount, we pay up to 90% of the total costs, reduced by taxes
(1-settings.LJP_TAX) * 0.9 * (float(self.total_bills_not_covered) + float(self.total_staff) ), (1-settings.LJP_TAX) * 0.9 * (float(self.total_bills_not_covered) + float(self.total_staff) ),
# we never pay more than the maximum costs of the trip
float(self.total_bills_not_covered) float(self.total_bills_not_covered)
) )
) )

@ -5,8 +5,9 @@ from django.conf import settings
from .models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction,\ from .models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction,\
StatementUnSubmittedManager, StatementSubmittedManager, StatementConfirmedManager,\ StatementUnSubmittedManager, StatementSubmittedManager, StatementConfirmedManager,\
StatementConfirmed, TransactionIssue, StatementManager StatementConfirmed, TransactionIssue, StatementManager
from members.models import Member, Group, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, NewMemberOnList,\ from members.models import Member, Group, Freizeit, LJPProposal, Intervention, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, NewMemberOnList,\
FAHRGEMEINSCHAFT_ANREISE, MALE, FEMALE, DIVERSE FAHRGEMEINSCHAFT_ANREISE, MALE, FEMALE, DIVERSE
from dateutil.relativedelta import relativedelta
# Create your tests here. # Create your tests here.
class StatementTestCase(TestCase): class StatementTestCase(TestCase):
@ -66,6 +67,116 @@ class StatementTestCase(TestCase):
email=settings.TEST_MAIL, gender=DIVERSE) email=settings.TEST_MAIL, gender=DIVERSE)
mol = NewMemberOnList.objects.create(member=m, memberlist=ex) mol = NewMemberOnList.objects.create(member=m, memberlist=ex)
ex.membersonlist.add(mol) ex.membersonlist.add(mol)
base = timezone.now()
ex = Freizeit.objects.create(name='Wild trip with old people', kilometers_traveled=self.kilometers_traveled,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=2, date=timezone.datetime(2024, 1, 2, 8, 0, 0, tzinfo=base.tzinfo), end=timezone.datetime(2024, 1, 5, 17, 0, 0, tzinfo=base.tzinfo) )
settings.EXCURSION_ORG_FEE = 20
settings.LJP_TAX = 0.2
settings.LJP_CONTRIBUTION_PER_DAY = 20
self.st5 = Statement.objects.create(night_cost=self.night_cost, excursion=ex)
for i in range(9):
m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date() - relativedelta(years=i+21),
email=settings.TEST_MAIL, gender=DIVERSE)
mol = NewMemberOnList.objects.create(member=m, memberlist=ex)
ex.membersonlist.add(mol)
ljpproposal = LJPProposal.objects.create(
title='Test proposal',
category=LJPProposal.LJP_STAFF_TRAINING,
goal=LJPProposal.LJP_ENVIRONMENT,
goal_strategy='my strategy',
not_bw_reason=LJPProposal.NOT_BW_ROOMS,
excursion=self.st5.excursion)
for i in range(3):
int = Intervention.objects.create(
date_start=timezone.datetime(2024, 1, 2+i, 12, 0, 0, tzinfo=base.tzinfo),
duration = 2+i,
activity = 'hi',
ljp_proposal=ljpproposal
)
self.b1 = Bill.objects.create(
statement=self.st5,
short_description='covered bill',
explanation='hi',
amount='300',
paid_by=self.fritz,
costs_covered=True,
refunded=False
)
self.b2 = Bill.objects.create(
statement=self.st5,
short_description='non-covered bill',
explanation='hi',
amount='900',
paid_by=self.fritz,
costs_covered=False,
refunded=False
)
def test_org_fee(self):
# org fee should be collected if participants are older than 26
self.assertEqual(self.st5.excursion.old_participant_count, 3, 'Calculation of number of old people in excursion is incorrect.')
total_org = 4 * 3 * 20 # 4 days, 3 old people, 20€ per day
self.assertEqual(self.st5.total_org_fee_theoretical, total_org, 'Theoretical org_fee should equal to amount per day per person * n_persons * n_days if there are old people.')
self.assertEqual(self.st5.total_org_fee, 0, 'Paid org fee should be 0 if no allowance and subsidies are paid if there are old people.')
self.assertIsNone(self.st5.org_fee_payant)
# now collect subsidies
self.st5.subsidy_to = self.fritz
self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies are paid.')
# now collect allowances
self.st5.allowance_to.add(self.fritz)
self.st5.subsidy_to = None
self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if allowances are paid.')
# now collect both
self.st5.subsidy_to = self.fritz
self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies and allowances are paid.')
self.assertEqual(self.st5.org_fee_payant, self.fritz, 'Org fee payant should be the receiver allowances and subsidies.')
# return to previous state
self.st5.subsidy_to = None
self.st5.allowance_to.remove(self.fritz)
def test_ljp_payment(self):
expected_intervention_hours = 2 + 3 + 4
expected_seminar_days = 0 + 0.5 + 0.5 # >=2.5h = 0.5days, >=5h = 1.0day
expected_ljp = (1-settings.LJP_TAX) * expected_seminar_days * settings.LJP_CONTRIBUTION_PER_DAY * 9
# (1 - 20% tax) * 1 seminar day * 20€ * 9 participants
self.assertEqual(self.st5.excursion.total_intervention_hours, expected_intervention_hours, 'Calculation of total intervention hours is incorrect.')
self.assertEqual(self.st5.excursion.total_seminar_days, expected_seminar_days, 'Calculation of total seminar days is incorrect.')
self.assertEqual(self.st5.paid_ljp_contributions, 0, 'No LJP contributions should be paid if no receiver is set.')
# now we want to pay out the LJP contributions
self.st5.ljp_to = self.fritz
self.assertEqual(self.st5.paid_ljp_contributions, expected_ljp, 'LJP contributions should be paid if a receiver is set.')
# now the total costs paid by trip organisers is lower than expected ljp contributions, should be reduced automatically
self.b2.amount=100
self.b2.save()
self.assertEqual(self.st5.total_bills_not_covered, 100, 'Changes in bills should be reflected in the total costs paid by trip organisers')
self.assertGreaterEqual(self.st5.total_bills_not_covered, self.st5.paid_ljp_contributions, 'LJP contributions should be less than or equal to the costs paid by trip organisers')
self.st5.ljp_to = None
def test_staff_count(self): def test_staff_count(self):
self.assertEqual(self.st4.admissible_staff_count, 0, self.assertEqual(self.st4.admissible_staff_count, 0,

@ -1436,23 +1436,30 @@ class Freizeit(CommonModel):
@property @property
def maximal_ljp_contributions(self): def maximal_ljp_contributions(self):
"""This is the maximal amount of LJP contributions that can be requested given participants and length
This calculation if intended for the LJP application, not for the payout."""
return cvt_to_decimal(settings.LJP_CONTRIBUTION_PER_DAY * self.ljp_participant_count * self.duration) return cvt_to_decimal(settings.LJP_CONTRIBUTION_PER_DAY * self.ljp_participant_count * self.duration)
@property @property
def potential_ljp_contributions(self): def potential_ljp_contributions(self):
"""The maximal amount can be reduced if the actual costs are lower than the maximal amount
This calculation if intended for the LJP application, not for the payout."""
if not hasattr(self, 'statement'):
return cvt_to_decimal(0)
return cvt_to_decimal(min(self.maximal_ljp_contributions, return cvt_to_decimal(min(self.maximal_ljp_contributions,
0.9 * float(self.statement.total_bills_theoretic) + float(self.statement.total_staff))) 0.9 * float(self.statement.total_bills_theoretic) + float(self.statement.total_staff)))
@property @property
def payable_ljp_contributions(self): def payable_ljp_contributions(self):
"""from the requested ljp contributions, a tax may be deducted for risk reduction""" """the payable contributions can differ from potential contributions if a tax is deducted for risk reduction.
if self.statement.ljp_to: the actual payout depends on more factors, e.g. the actual costs that had to be paid by the trip organisers."""
if hasattr(self, 'statement') and self.statement.ljp_to:
return self.statement.paid_ljp_contributions return self.statement.paid_ljp_contributions
return cvt_to_decimal(self.potential_ljp_contributions * cvt_to_decimal(1 - settings.LJP_TAX)) return cvt_to_decimal(self.potential_ljp_contributions * cvt_to_decimal(1 - settings.LJP_TAX))
@property @property
def total_relative_costs(self): def total_relative_costs(self):
if not self.statement: if not hasattr(self, 'statement'):
return 0 return 0
total_costs = self.statement.total_bills_theoretic total_costs = self.statement.total_bills_theoretic
total_contributions = self.statement.total_subsidies + self.payable_ljp_contributions total_contributions = self.statement.total_subsidies + self.payable_ljp_contributions

Loading…
Cancel
Save