Coverage for jacc/tests.py: 100%

# Coverage for jacc/tests.py : 100%

## 234 statements 234 run 0 missing 0 excluded

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

1 from decimal import Decimal

2 from datetime import timedelta, datetime, date

3 import pytz

4 from jacc.interests import calculate_simple_interest

5 from jacc.models import AccountEntry, Account, Invoice, EntryType, AccountType, INVOICE_CREDIT_NOTE

6 from django.test import TestCase

7 from django.utils.timezone import now

8 from jacc.settle import settle_assigned_invoice, settle_credit_note

9 from jutil.dates import add_month

10 from jutil.format import dec2

11 from jutil.parse import parse_datetime

12 from jutil.testing import TestSetupMixin

13

14

15 ACCOUNT_RECEIVABLES = 'RE'

16 ACCOUNT_SETTLEMENTS = 'SE'

17 E_SETTLEMENT = 'SE'

18 E_MANUAL_SETTLEMENT = 'MS'

19 E_RENT = 'IR'

20 E_FEE = 'FE'

21 E_CAPITAL = 'CA'

22 E_INTEREST = 'IN'

23 E_OVERPAYMENT = 'OP'

24 E_CREDIT_NOTE_RECONCILIATION = '33'

25

26 INTEREST_ACCUMULATING_TYPE_CODES = [E_CAPITAL]

27 INTEREST_TYPE_CODES = [E_INTEREST]

28

29

30 def 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

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

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

41

42

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