Hide keyboard shortcuts

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 

13 

14 

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' 

25 

26INTEREST_ACCUMULATING_TYPE_CODES = [E_CAPITAL] 

27INTEREST_TYPE_CODES = [E_INTEREST] 

28 

29 

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

37 

38 

39def make_datetime(year, month, day) -> datetime: 

40 return pytz.utc.localize(datetime(year=year, month=month, day=day)) 

41 

42 

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) 

103 

104 def tearDown(self): 

105 pass 

106 

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]) 

127 

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) 

133 

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] 

155 

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

184 

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) 

190 

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] 

207 

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

228 

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')) 

238 

239 def test_settlements_with_assigned_invoices(self): 

240 print('test_settlements_with_assigned_invoices') 

241 

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) 

247 

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

262 

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) 

279 

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')) 

298 

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')) 

316 

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')) 

328 

329 def test_credit_note(self): 

330 print('test_credit_note') 

331 

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) 

347 

348 invoice.update_cached_fields() 

349 

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')) 

355 

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) 

365 

366 credit_note.update_cached_fields() 

367 

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')) 

373 

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) 

379 

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'))