From bf75212d267f98d0701b22bc5783128cd6f0399e Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Tue, 4 Apr 2023 22:06:06 +0200 Subject: [PATCH] finance: add tests for model mechanics, fix bug in transaction issue calculation --- jdav_web/finance/models.py | 15 ++- jdav_web/finance/tests.py | 182 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py index d66d12b..4d2cdbd 100644 --- a/jdav_web/finance/models.py +++ b/jdav_web/finance/models.py @@ -111,14 +111,14 @@ class Statement(CommonModel): needed_paiments.extend([(yl, self.real_per_yl) for yl in self.excursion.jugendleiter.all()]) needed_paiments = sorted(needed_paiments, key=lambda p: p[0].pk) - target = map(lambda p: (p[0], sum([x[1] for x in p[1]])), groupby(needed_paiments, lambda p: p[0])) + target = dict(map(lambda p: (p[0], sum([x[1] for x in p[1]])), groupby(needed_paiments, lambda p: p[0]))) transactions = sorted(self.transaction_set.all(), key=lambda trans: trans.member.pk) current = dict(map(lambda p: (p[0], sum([t.amount for t in p[1]])), groupby(transactions, lambda trans: trans.member))) issues = [] - for member, amount in target: - if amount == 0: + for member, amount in target.items(): + if amount == 0 and member not in current: continue elif member not in current: issue = TransactionIssue(member=member, current=0, target=amount) @@ -126,6 +126,12 @@ class Statement(CommonModel): elif current[member] != amount: issue = TransactionIssue(member=member, current=current[member], target=amount) issues.append(issue) + + for member, amount in current.items(): + if amount != 0 and member not in target: + issue = TransactionIssue(member=member, current=amount, target=0) + issues.append(issue) + return issues @property @@ -151,6 +157,9 @@ class Statement(CommonModel): return Statement.VALID def confirm(self, confirmer=None): + if not self.submitted: + return False + if not self.validity == Statement.VALID: return False diff --git a/jdav_web/finance/tests.py b/jdav_web/finance/tests.py index 7ce503c..0014d8d 100644 --- a/jdav_web/finance/tests.py +++ b/jdav_web/finance/tests.py @@ -1,3 +1,185 @@ from django.test import TestCase +from django.utils import timezone +from django.conf import settings +from .models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction +from members.models import Member, Group, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, NewMemberOnList,\ + FAHRGEMEINSCHAFT_ANREISE # Create your tests here. +class StatementTestCase(TestCase): + night_cost = 27 + kilometers_traveled = 512 + participant_count = 10 + staff_count = 5 + + def setUp(self): + self.jl = Group.objects.create(name="Jugendleiter") + self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), + email=settings.TEST_MAIL) + self.fritz.group.add(self.jl) + self.fritz.save() + + self.personal_account = Ledger.objects.create(name='personal account') + + self.st = Statement.objects.create(short_description='A statement', explanation='Important!', night_cost=0) + Bill.objects.create(statement=self.st, short_description='food', explanation='i was hungry', + amount=67.3, costs_covered=False, paid_by=self.fritz) + Transaction.objects.create(reference='gift', amount=12.3, + ledger=self.personal_account, member=self.fritz, + statement=self.st) + + self.st2 = Statement.objects.create(short_description='Actual expenses', night_cost=0) + Bill.objects.create(statement=self.st2, short_description='food', explanation='i was hungry', + amount=67.3, costs_covered=True, paid_by=self.fritz) + + ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=self.kilometers_traveled, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=1) + self.st3 = Statement.objects.create(night_cost=self.night_cost, excursion=ex) + for i in range(self.participant_count): + m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), + email=settings.TEST_MAIL) + mol = NewMemberOnList.objects.create(member=m, memberlist=ex) + ex.membersonlist.add(mol) + for i in range(self.staff_count): + m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), + email=settings.TEST_MAIL) + Bill.objects.create(statement=self.st3, short_description='food', explanation='i was hungry', + amount=42.69, costs_covered=True, paid_by=m) + m.group.add(self.jl) + ex.jugendleiter.add(m) + + ex = Freizeit.objects.create(name='Wild trip 2', kilometers_traveled=self.kilometers_traveled, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=2) + self.st4 = Statement.objects.create(night_cost=self.night_cost, excursion=ex) + for i in range(2): + m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), + email=settings.TEST_MAIL) + mol = NewMemberOnList.objects.create(member=m, memberlist=ex) + ex.membersonlist.add(mol) + + def test_staff_count(self): + self.assertEqual(self.st4.admissible_staff_count, 0, + 'Admissible staff count is not 0, although not enough participants.') + for i in range(2): + m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), + email=settings.TEST_MAIL) + mol = NewMemberOnList.objects.create(member=m, memberlist=self.st4.excursion) + self.st4.excursion.membersonlist.add(mol) + self.assertEqual(self.st4.admissible_staff_count, 2, + 'Admissible staff count is not 2, although there are 4 participants.') + + def test_reduce_transactions(self): + self.st3.generate_transactions() + self.assertEqual(self.st3.transaction_set.count(), self.staff_count * 2, + 'Transaction count is not twice the staff count.') + self.st3.reduce_transactions() + self.assertEqual(self.st3.transaction_set.count(), self.staff_count * 2, + 'Transaction count after reduction is not the same as before, although no ledgers are configured.') + for trans in self.st3.transaction_set.all(): + trans.ledger = self.personal_account + trans.save() + self.st3.reduce_transactions() + self.assertEqual(self.st3.transaction_set.count(), self.staff_count, + 'Transaction count after setting ledgers and reduction is not halved.') + self.st3.reduce_transactions() + self.assertEqual(self.st3.transaction_set.count(), self.staff_count, + 'Transaction count did change after reducing a second time.') + + def test_confirm_statement(self): + self.assertFalse(self.st3.confirm(confirmer=self.fritz), 'Statement was confirmed, although it is not submitted.') + self.st3.submit(submitter=self.fritz) + self.assertTrue(self.st3.submitted, 'Statement is not submitted, although it was.') + self.assertEqual(self.st3.submitted_by, self.fritz, + 'Statement was not submitted by fritz.') + + self.assertFalse(self.st3.confirm(), 'Statement was confirmed, but is not valid yet.') + self.st3.generate_transactions() + for trans in self.st3.transaction_set.all(): + trans.ledger = self.personal_account + trans.save() + self.assertTrue(self.st3.confirm(confirmer=self.fritz), + 'Statement was not confirmed, although it submitted and valid.') + self.assertEqual(self.st3.confirmed_by, self.fritz, 'Statement not confirmed by fritz.') + for trans in self.st3.transaction_set.all(): + self.assertTrue(trans.confirmed, 'Transaction on confirmed statement is not confirmed.') + self.assertEqual(trans.confirmed_by, self.fritz, 'Transaction on confirmed statement is not confirmed by fritz.') + + def test_excursion_statement(self): + self.assertEqual(self.st3.excursion.staff_count, self.staff_count, + 'Calculated staff count is not constructed staff count.') + self.assertEqual(self.st3.excursion.participant_count, self.participant_count, + 'Calculated participant count is not constructed participant count.') + self.assertLess(self.st3.admissible_staff_count, self.staff_count, + 'All staff members are refinanced, although {} is too much for {} participants.'.format(self.staff_count, self.participant_count)) + self.assertFalse(self.st3.transactions_match_expenses, + 'Transactions match expenses, but currently no one is paid.') + self.assertGreater(self.st3.total_staff, 0, + 'There are no costs for the staff, although there are enough participants.') + self.assertEqual(self.st3.total_nights, 0, + 'There are costs for the night, although there was no night.') + self.assertEqual(self.st3.real_night_cost, settings.MAX_NIGHT_COST, + 'Real night cost is not the max, although the given one is way too high.') + # changing means of transport changes euro_per_km + epkm = self.st3.euro_per_km + self.st3.excursion.tour_approach = FAHRGEMEINSCHAFT_ANREISE + self.assertNotEqual(epkm, self.st3.euro_per_km, 'Changing means of transport did not change euro per km.') + self.st3.generate_transactions() + self.assertTrue(self.st3.transactions_match_expenses, + "Transactions don't match expenses after generating them.") + self.assertGreater(self.st3.total, 0, 'Total is 0.') + + def test_generate_transactions(self): + # self.st2 has an unpaid bill + self.assertFalse(self.st2.transactions_match_expenses, + 'Transactions match expenses, but one bill is not paid.') + self.st2.generate_transactions() + # now transactions should match expenses + self.assertTrue(self.st2.transactions_match_expenses, + "Transactions don't match expenses after generating them.") + # self.st2 is still not valid + self.assertEqual(self.st2.validity, Statement.MISSING_LEDGER, + 'Statement is valid, although transaction has no ledger setup.') + for trans in self.st2.transaction_set.all(): + trans.ledger = self.personal_account + trans.save() + self.assertEqual(self.st2.validity, Statement.VALID, + 'Statement is still invalid, after setting up ledger.') + + # create a new transaction issue by manually changing amount + t1 = self.st2.transaction_set.all()[0] + t1.amount = 123 + t1.save() + self.assertFalse(self.st2.transactions_match_expenses, + 'Transactions match expenses, but one transaction was tweaked.') + + def test_statement_without_excursion(self): + # should be all 0, since no excursion is associated + self.assertEqual(self.st.real_staff_count, 0) + self.assertEqual(self.st.admissible_staff_count, 0) + self.assertEqual(self.st.nights_per_yl, 0) + self.assertEqual(self.st.allowance_per_yl, 0) + self.assertEqual(self.st.real_per_yl, 0) + self.assertEqual(self.st.transportation_per_yl, 0) + self.assertEqual(self.st.euro_per_km, 0) + self.assertEqual(self.st.total_allowance, 0) + self.assertEqual(self.st.total_transportation, 0) + + def test_detect_unallowed_gift(self): + # there is a bill + self.assertGreater(self.st.total_bills_theoretic, 0, 'Theoretic bill total is 0 (should be > 0).') + # but it is not covered + self.assertEqual(self.st.total_bills, 0, 'Real bill total is not 0.') + self.assertEqual(self.st.total, 0, 'Total is not 0.') + self.assertGreater(self.st.total_theoretic, 0, 'Total in theorey is 0.') + self.st.generate_transactions() + self.assertEqual(self.st.transaction_set.count(), 1, 'Generating transactions did produce new transactions.') + # but there is a transaction anyway + self.assertFalse(self.st.transactions_match_expenses, + 'Transactions match expenses, although an unreasonable gift is paid.') + # so statement must be invalid + self.assertFalse(self.st.is_valid(), + 'Transaction is valid, although an unreasonable gift is paid.')