Coverage for jacc/tests.py : 100%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from decimal import Decimal
2from datetime import timedelta, datetime, date
3import pytz
4from jacc.interests import calculate_simple_interest
5from jacc.models import AccountEntry, Account, Invoice, EntryType, AccountType, INVOICE_CREDIT_NOTE
6from django.test import TestCase
7from django.utils.timezone import now
8from jacc.settle import settle_assigned_invoice, settle_credit_note
9from jutil.dates import add_month
10from jutil.format import dec2
11from jutil.parse import parse_datetime
12from jutil.testing import TestSetupMixin
15ACCOUNT_RECEIVABLES = 'RE'
16ACCOUNT_SETTLEMENTS = 'SE'
17E_SETTLEMENT = 'SE'
18E_MANUAL_SETTLEMENT = 'MS'
19E_RENT = 'IR'
20E_FEE = 'FE'
21E_CAPITAL = 'CA'
22E_INTEREST = 'IN'
23E_OVERPAYMENT = 'OP'
24E_CREDIT_NOTE_RECONCILIATION = '33'
26INTEREST_ACCUMULATING_TYPE_CODES = [E_CAPITAL]
27INTEREST_TYPE_CODES = [E_INTEREST]
30def create_account_by_type(type_id: str):
31 """
32 Create account by AccountType id.
33 :param type_id:
34 :return: Account
35 """
36 return Account.objects.create(type=AccountType.objects.get(code=type_id))
39def make_datetime(year, month, day) -> datetime:
40 return pytz.utc.localize(datetime(year=year, month=month, day=day))
43class Tests(TestCase, TestSetupMixin):
44 def setUp(self):
45 self.user = self.add_test_user()
46 AccountType.objects.create(code=ACCOUNT_RECEIVABLES, name='Receivables', is_asset=True)
47 AccountType.objects.create(code=ACCOUNT_SETTLEMENTS, name='Settlements', is_asset=True)
48 ae_types = [
49 {
50 "code": "CA",
51 "name": "pääoma",
52 "payback_priority": 3,
53 },
54 {
55 "code": "OP",
56 "name": "liikasuoritus",
57 "payback_priority": 3,
58 },
59 {
60 "code": "CN",
61 "name": "peruutus"
62 },
63 {
64 "code": "CO",
65 "name": "korjaus"
66 },
67 {
68 "code": "FE",
69 "name": "nostopalkkio",
70 "payback_priority": 2,
71 },
72 {
73 "code": "FP",
74 "name": "loppusuoritus"
75 },
76 {
77 "code": "IN",
78 "name": "korko",
79 "payback_priority": 1,
80 },
81 {
82 "code": "MS",
83 "name": "ohisuoritus",
84 "is_settlement": True,
85 },
86 {
87 "code": "SE",
88 "name": "suoritus",
89 "is_settlement": True,
90 },
91 {
92 "code": "IR",
93 "name": "vuokra"
94 },
95 {
96 "code": E_CREDIT_NOTE_RECONCILIATION,
97 "name": 'hyvityslaskun kohdistus',
98 "is_settlement": True,
99 }
100 ]
101 for ae_type in ae_types:
102 EntryType.objects.create(**ae_type)
104 def tearDown(self):
105 pass
107 def test_account(self):
108 print('test_account')
109 settlements = create_account_by_type(ACCOUNT_SETTLEMENTS)
110 assert isinstance(settlements, Account)
111 amounts = [12, '13.12', '-1.23', '20.00']
112 balances = [Decimal('12.00'), Decimal('25.12'), Decimal('23.89'), Decimal('43.89')]
113 t = parse_datetime('2016-06-13T01:00:00')
114 dt = timedelta(minutes=5)
115 times = [t+dt*i for i in range(len(amounts))]
116 for i in range(len(times)):
117 amount = amounts[i]
118 t = times[i]
119 e = AccountEntry(account=settlements, amount=Decimal(amount), type=EntryType.objects.get(code=E_SETTLEMENT), timestamp=t)
120 e.full_clean()
121 e.save()
122 self.assertEqual(settlements.balance, balances[i])
123 self.assertEqual(settlements.balance, e.balance)
124 for i in range(len(times)):
125 t = times[i]
126 self.assertEqual(settlements.get_balance(t+timedelta(seconds=1)), balances[i])
128 def test_invoice(self):
129 print('test_invoice')
130 settlements = create_account_by_type(ACCOUNT_SETTLEMENTS)
131 receivables_acc = create_account_by_type(ACCOUNT_RECEIVABLES)
132 assert isinstance(settlements, Account)
134 # create invoices
135 t = parse_datetime('2016-05-05')
136 amounts = [
137 Decimal('120.00'),
138 Decimal('100.00'),
139 Decimal('50.00'),
140 Decimal('40.00'),
141 ]
142 n = len(amounts)
143 times = [add_month(t, i) for i in range(n)]
144 invoices = []
145 for t, amount in zip(times, amounts):
146 invoice = Invoice(due_date=t)
147 invoice.full_clean()
148 invoice.save()
149 AccountEntry.objects.create(account=receivables_acc, source_invoice=invoice, type=EntryType.objects.get(code=E_RENT), amount=amount)
150 invoice.update_cached_fields()
151 self.assertEqual(invoice.unpaid_amount, amount)
152 invoices.append(invoice)
153 # print(invoice)
154 unpaid_invoices = [i for i in invoices]
156 # create payments
157 payment_ops = [
158 (None, [Decimal('120.00'), Decimal('100.00'), Decimal('50.00'), Decimal('40.00')]),
159 (Decimal('50.00'), [Decimal('70.00'), Decimal('100.00'), Decimal('50.00'), Decimal('40.00')]),
160 (Decimal('70.50'), [Decimal('00.00'), Decimal('100.00'), Decimal('50.00'), Decimal('40.00')]),
161 (Decimal('100.00'), [Decimal('00.00'), Decimal('00.00'), Decimal('50.00'), Decimal('40.00')]),
162 (Decimal('100.00'), [Decimal('00.00'), Decimal('00.00'), Decimal('00.00'), Decimal('40.00')]),
163 ]
164 for j in range(len(payment_ops)):
165 print('test_invoice: Payment op test', j)
166 for i in range(n):
167 inv = invoices[i]
168 assert isinstance(inv, Invoice)
169 paid_amount, unpaid_amounts = payment_ops[j]
170 if paid_amount is not None:
171 p = AccountEntry.objects.create(account=settlements, settled_invoice=unpaid_invoices[0], amount=paid_amount, type=EntryType.objects.get(code=E_MANUAL_SETTLEMENT))
172 settle_assigned_invoice(receivables_acc, p, AccountEntry)
173 if unpaid_invoices[0].is_paid:
174 unpaid_invoices = unpaid_invoices[1:]
175 for i in range(n):
176 inv = invoices[i]
177 assert isinstance(inv, Invoice)
178 for i in range(n):
179 paid_amount_real = invoices[i]
180 unpaid_amount_real = invoices[i].get_unpaid_amount()
181 unpaid_amount_ref = unpaid_amounts[i]
182 print('checking invoice {} payment status after payment op {} (real {}, expected {})'.format(i, j, unpaid_amount_real, unpaid_amount_ref))
183 self.assertEqual(unpaid_amount_real, unpaid_amount_ref, '[{}][{}]'.format(j, i))
185 # create another acc set
186 settlements = create_account_by_type(ACCOUNT_SETTLEMENTS)
187 assert isinstance(settlements, Account)
188 receivables_acc = create_account_by_type(ACCOUNT_RECEIVABLES)
189 assert isinstance(receivables_acc, Account)
191 # create invoices
192 t = parse_datetime('2016-05-05')
193 amounts = [Decimal('120.00'), Decimal('100.00'), Decimal('50.00'), Decimal('40.00')]
194 n = len(amounts)
195 times = [add_month(t, i) for i in range(n)]
196 invoices = []
197 for t, amount in zip(times, amounts):
198 invoice = Invoice(due_date=t)
199 invoice.full_clean()
200 invoice.save()
201 AccountEntry.objects.create(account=receivables_acc, source_invoice=invoice, type=EntryType.objects.get(code=E_RENT), amount=amount)
202 invoice.update_cached_fields()
203 self.assertEqual(invoice.unpaid_amount, amount)
204 invoices.append(invoice)
205 print('invoice created', invoice)
206 unpaid_invoices = [i for i in invoices]
208 # create payments:
209 # paid_amount, unpaid_amounts (after payment)
210 payment_ops = [
211 (None, [Decimal('120.00'), Decimal('100.00'), Decimal('50.00'), Decimal('40.00')]),
212 (Decimal('250.00'), [Decimal('0.00'), Decimal('100.00'), Decimal('50.00'), Decimal('40.00')]),
213 ]
214 for j in range(len(payment_ops)):
215 paid_amount, unpaid_amounts = payment_ops[j]
216 if paid_amount is not None and paid_amount > Decimal('0.00'):
217 invoice = unpaid_invoices[0]
218 print('Targeting settlement amount', paid_amount, 'to invoice', invoice, 'invoice.amount', invoice.amount)
219 p = AccountEntry.objects.create(account=settlements, amount=paid_amount, settled_invoice=invoice, type=EntryType.objects.get(code=E_MANUAL_SETTLEMENT))
220 settle_assigned_invoice(receivables_acc, p, AccountEntry)
221 if invoice.is_paid:
222 unpaid_invoices = unpaid_invoices[1:]
223 print('invoice paid, now left', unpaid_invoices)
224 for i in range(n):
225 unpaid_amount_real = invoices[i].get_unpaid_amount()
226 unpaid_amount_ref = unpaid_amounts[i]
227 self.assertEqual(unpaid_amount_real, unpaid_amount_ref, '[{}][{}]'.format(j, i))
229 # check that the first payment has E_RENT 120
230 inv = invoices[0]
231 assert isinstance(inv, Invoice)
232 es = inv.receivables.order_by('id')
233 self.assertEqual(es[0].amount, Decimal('120.00'))
234 self.assertEqual(es[1].amount, Decimal('-120.00'))
235 self.assertIsNotNone(es[1].parent)
236 self.assertTrue(es[1].parent.type.is_settlement)
237 self.assertEqual(es[1].parent.amount, Decimal('250.00'))
239 def test_settlements_with_assigned_invoices(self):
240 print('test_settlements_with_assigned_invoices')
242 e_capital = EntryType.objects.get(code=E_CAPITAL)
243 e_fee = EntryType.objects.get(code=E_FEE)
244 e_interest = EntryType.objects.get(code=E_INTEREST)
245 e_settlement = EntryType.objects.get(code=E_SETTLEMENT)
246 e_overpayment = EntryType.objects.get(code=E_OVERPAYMENT)
248 # invoice: cap 100, fee 10, interest 5
249 invoice_components = [
250 (e_capital, 100),
251 (e_fee, 10),
252 (e_interest, 5),
253 ]
254 settlement_acc = Account.objects.create(type=AccountType.objects.get(code=ACCOUNT_SETTLEMENTS))
255 receivables_acc = Account.objects.create(type=AccountType.objects.get(code=ACCOUNT_RECEIVABLES))
256 invoice = Invoice.objects.create(due_date=now())
257 assert isinstance(receivables_acc, Account)
258 assert isinstance(settlement_acc, Account)
259 assert isinstance(invoice, Invoice)
260 for ae_type, amt in invoice_components:
261 AccountEntry.objects.create(account=receivables_acc, source_invoice=invoice, type=ae_type, amount=Decimal(amt))
263 # paybacks
264 # order (see setUp): cap, fee, interest
265 paybacks_and_unpaid_components = [
266 (None, 115),
267 (20, 95),
268 (80, 15),
269 (10, 5),
270 (5, 0),
271 ]
272 AccountEntry.objects.distinct('type').order_by('type__payback_order')
273 for payback, unpaid in paybacks_and_unpaid_components:
274 if payback:
275 payback = AccountEntry.objects.create(account=settlement_acc, settled_invoice=invoice, type=e_settlement, amount=Decimal(payback))
276 settle_assigned_invoice(receivables_acc, payback, AccountEntry)
277 bal = invoice.get_balance(invoice.receivables_account)
278 self.assertEqual(bal, unpaid)
280 def test_calculate_simple_interest(self):
281 print('test_calculate_simple_interest')
282 apr = Decimal('48.74')
283 capital = Decimal('500.00')
284 et_capital = EntryType.objects.get(code=E_CAPITAL)
285 entries = [
286 AccountEntry(type=et_capital, amount=capital, timestamp=make_datetime(2017, 1, 1)),
287 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 3, 1)),
288 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 5, 1)),
289 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 7, 1)),
290 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 9, 1)),
291 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 11, 1)),
292 AccountEntry(type=et_capital, amount=Decimal('-437.50'), timestamp=make_datetime(2018, 1, 1)),
293 ]
294 timestamp = make_datetime(2018, 1, 1)
295 interest = calculate_simple_interest(entries, apr, timestamp.date())
296 print('interest =', dec2(interest))
297 self.assertEqual(interest.quantize(Decimal('1.00')), Decimal('182.41'))
299 def test_calculate_simple_interest2(self):
300 print('test_calculate_simple_interest2')
301 apr = Decimal('48.74')
302 capital = Decimal('500.00')
303 et_capital = EntryType.objects.get(code=E_CAPITAL)
304 entries = [
305 AccountEntry(type=et_capital, amount=capital, timestamp=make_datetime(2017, 1, 1)),
306 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 3, 1)),
307 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 5, 1)),
308 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 7, 1)),
309 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 9, 1)),
310 AccountEntry(type=et_capital, amount=Decimal(-50), timestamp=make_datetime(2017, 11, 1)),
311 ]
312 timestamp = make_datetime(2020, 1, 1)
313 interest = calculate_simple_interest(entries, apr, timestamp.date())
314 print('interest =', dec2(interest))
315 self.assertEqual(interest.quantize(Decimal('1.00')), Decimal('426.11'))
317 def test_calculate_simple_interest3(self):
318 print('test_calculate_simple_interest3')
319 apr = Decimal('3.00')
320 capital = Decimal('500.00')
321 et_capital = EntryType.objects.get(code=E_CAPITAL)
322 entries = [
323 AccountEntry(type=et_capital, amount=capital, timestamp=make_datetime(2018, 1, 10)),
324 ]
325 interest = calculate_simple_interest(entries, apr, date(2018, 3, 1), begin=date(2018, 2, 10))
326 print('interest =', dec2(interest))
327 self.assertEqual(interest.quantize(Decimal('1.00')), Decimal('0.78'))
329 def test_credit_note(self):
330 print('test_credit_note')
332 # create invoice
333 e_capital = EntryType.objects.get(code=E_CAPITAL)
334 e_fee = EntryType.objects.get(code=E_FEE)
335 invoice_components = [
336 (e_capital, Decimal(100)),
337 (e_fee, Decimal(10)),
338 ]
339 settlement_acc = Account.objects.create(type=AccountType.objects.get(code=ACCOUNT_SETTLEMENTS))
340 receivables_acc = Account.objects.create(type=AccountType.objects.get(code=ACCOUNT_RECEIVABLES))
341 invoice = Invoice.objects.create(due_date=now())
342 assert isinstance(receivables_acc, Account)
343 assert isinstance(settlement_acc, Account)
344 assert isinstance(invoice, Invoice)
345 for ae_type, amt in invoice_components:
346 AccountEntry.objects.create(account=receivables_acc, source_invoice=invoice, type=ae_type, amount=amt)
348 invoice.update_cached_fields()
350 # ensure balances
351 self.assertEqual(invoice.get_unpaid_amount(), Decimal('110.00'))
352 self.assertEqual(invoice.get_paid_amount(), Decimal('0.00'))
353 self.assertEqual(invoice.get_amount(), Decimal('110.00'))
354 self.assertEqual(invoice.get_overpaid_amount(), Decimal('0.00'))
356 # create credit note
357 e_capital = EntryType.objects.get(code=E_CAPITAL)
358 invoice_components = [
359 (e_capital, Decimal(-110)),
360 ]
361 credit_note = Invoice.objects.create(due_date=now(), type=INVOICE_CREDIT_NOTE)
362 assert isinstance(credit_note, Invoice)
363 for ae_type, amt in invoice_components:
364 AccountEntry.objects.create(account=receivables_acc, source_invoice=credit_note, type=ae_type, amount=amt)
366 credit_note.update_cached_fields()
368 # ensure balances
369 self.assertEqual(credit_note.get_unpaid_amount(), Decimal('-110.00'))
370 self.assertEqual(credit_note.get_paid_amount(), Decimal('0.00'))
371 self.assertEqual(credit_note.get_amount(), Decimal('-110.00'))
372 self.assertEqual(credit_note.get_overpaid_amount(), Decimal('0.00'))
374 # settle credit note
375 e_credit_note = EntryType.objects.get(code=E_CREDIT_NOTE_RECONCILIATION)
376 pmts = settle_credit_note(credit_note, invoice, AccountEntry, settlement_acc, entry_type=e_credit_note)
377 for pmt in pmts:
378 settle_assigned_invoice(receivables_acc, pmt, AccountEntry)
380 # ensure balances
381 self.assertEqual(credit_note.get_unpaid_amount(), Decimal('0.00'))
382 self.assertEqual(credit_note.get_paid_amount(), Decimal('-110.00'))
383 self.assertEqual(credit_note.get_amount(), Decimal('-110.00'))
384 self.assertEqual(credit_note.get_overpaid_amount(), Decimal('0.00'))
385 self.assertEqual(invoice.get_unpaid_amount(), Decimal('0.00'))
386 self.assertEqual(invoice.get_paid_amount(), Decimal('110.00'))
387 self.assertEqual(invoice.get_amount(), Decimal('110.00'))
388 self.assertEqual(invoice.get_overpaid_amount(), Decimal('0.00'))