Add trade finance ledger foundation
This commit is contained in:
@@ -5,6 +5,7 @@ from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.modules.company.tests import create_company, set_company
|
||||
from trytond.modules.currency.tests import create_currency
|
||||
from trytond.pool import Pool
|
||||
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
||||
@@ -35,6 +36,46 @@ class TradeFinanceTestCase(ModuleTestCase):
|
||||
facility.save()
|
||||
return facility, currency
|
||||
|
||||
def create_trade_finance_case(self):
|
||||
pool = Pool()
|
||||
Cost = pool.get('trade_finance.facility_limit_cost')
|
||||
Haircut = pool.get('trade_finance.facility_limit_haircut')
|
||||
Limit = pool.get('trade_finance.facility_limit')
|
||||
TradeFinance = pool.get('trade.finance')
|
||||
|
||||
facility, currency = self.create_facility()
|
||||
company = create_company(currency=currency)
|
||||
limit, = Limit.create([{
|
||||
'facility': facility.id,
|
||||
'name': 'Prefi limit',
|
||||
'currency': currency.id,
|
||||
'amount': Decimal('1000000.00'),
|
||||
'date_from': date(2026, 1, 1),
|
||||
'date_to': date(2026, 12, 31),
|
||||
}])
|
||||
Haircut.create([{
|
||||
'limit': limit.id,
|
||||
'attribute': 'commodity',
|
||||
'haircut_pct': Decimal('20.00'),
|
||||
'date_from': date(2026, 1, 1),
|
||||
'date_to': date(2026, 12, 31),
|
||||
}])
|
||||
Cost.create([{
|
||||
'limit': limit.id,
|
||||
'cost_type': 'interest',
|
||||
'spread': Decimal('10.0000'),
|
||||
'date_from': date(2026, 1, 1),
|
||||
'date_to': date(2026, 12, 31),
|
||||
}])
|
||||
with set_company(company):
|
||||
trade_finance, = TradeFinance.create([{
|
||||
'bank': facility.tfe.id,
|
||||
'company': company.id,
|
||||
'currency': currency.id,
|
||||
'start_date': date(2026, 1, 1),
|
||||
}])
|
||||
return trade_finance, limit, currency
|
||||
|
||||
@with_transaction()
|
||||
def test_sublimit_inherits_currency_and_dates(self):
|
||||
'Test sublimit inherits currency and dates from parent'
|
||||
@@ -141,5 +182,147 @@ class TradeFinanceTestCase(ModuleTestCase):
|
||||
|
||||
self.assertEqual(child.currency, other_currency)
|
||||
|
||||
@with_transaction()
|
||||
def test_trade_finance_number_sequence(self):
|
||||
'Test trade finance number sequence'
|
||||
trade_finance, _, _ = self.create_trade_finance_case()
|
||||
|
||||
self.assertTrue(trade_finance.number.startswith('TFR-'))
|
||||
|
||||
@with_transaction()
|
||||
def test_post_presentation_uses_facility_limit_ledger(self):
|
||||
'Test posting creates an audited facility limit movement'
|
||||
pool = Pool()
|
||||
Presentation = pool.get('trade.finance.presentation')
|
||||
Line = pool.get('trade.finance.presentation.line')
|
||||
|
||||
trade_finance, limit, currency = self.create_trade_finance_case()
|
||||
presentation, = Presentation.create([{
|
||||
'trade_finance': trade_finance.id,
|
||||
'presentation_date': date(2026, 1, 10),
|
||||
'presentation_type': 'prefi',
|
||||
'finance_stage': 'prefi',
|
||||
}])
|
||||
line, = Line.create([{
|
||||
'presentation': presentation.id,
|
||||
'facility_limit': limit.id,
|
||||
'signed_quantity': Decimal('1000.0000'),
|
||||
'commodity_price': Decimal('100.000000'),
|
||||
}])
|
||||
|
||||
self.assertEqual(line.currency, currency)
|
||||
self.assertEqual(line.gross_collateral_value, Decimal('100000.0000000000'))
|
||||
self.assertEqual(line.financed_amount, Decimal('100000.0000000000'))
|
||||
Presentation.post([presentation])
|
||||
|
||||
presentation = Presentation(presentation.id)
|
||||
line, = presentation.lines
|
||||
self.assertEqual(presentation.state, 'posted')
|
||||
self.assertEqual(line.haircut_percent_applied, Decimal('20.00'))
|
||||
self.assertEqual(line.interest_rate_applied, Decimal('10.0000'))
|
||||
self.assertEqual(line.day_count_basis_applied, '360')
|
||||
self.assertEqual(line.financed_amount, Decimal('80000.000000000000'))
|
||||
self.assertEqual(
|
||||
line.signed_financed_amount, Decimal('80000.000000000000'))
|
||||
|
||||
@with_transaction()
|
||||
def test_post_presentation_rejects_limit_outside_validity(self):
|
||||
'Test posting validates facility limit validity dates'
|
||||
pool = Pool()
|
||||
Presentation = pool.get('trade.finance.presentation')
|
||||
Line = pool.get('trade.finance.presentation.line')
|
||||
|
||||
trade_finance, limit, _ = self.create_trade_finance_case()
|
||||
presentation, = Presentation.create([{
|
||||
'trade_finance': trade_finance.id,
|
||||
'presentation_date': date(2025, 12, 31),
|
||||
'presentation_type': 'prefi',
|
||||
'finance_stage': 'prefi',
|
||||
}])
|
||||
Line.create([{
|
||||
'presentation': presentation.id,
|
||||
'facility_limit': limit.id,
|
||||
'movement_date': date(2025, 12, 31),
|
||||
'finance_stage': 'prefi',
|
||||
'currency': trade_finance.currency.id,
|
||||
'signed_financed_amount': Decimal('100.00'),
|
||||
}])
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
Presentation.post([presentation])
|
||||
|
||||
@with_transaction()
|
||||
def test_cancel_presentation_creates_reversal(self):
|
||||
'Test cancellation creates a posted reversal movement'
|
||||
pool = Pool()
|
||||
Presentation = pool.get('trade.finance.presentation')
|
||||
Line = pool.get('trade.finance.presentation.line')
|
||||
|
||||
trade_finance, limit, _ = self.create_trade_finance_case()
|
||||
presentation, = Presentation.create([{
|
||||
'trade_finance': trade_finance.id,
|
||||
'presentation_date': date(2026, 1, 10),
|
||||
'presentation_type': 'prefi',
|
||||
'finance_stage': 'prefi',
|
||||
}])
|
||||
Line.create([{
|
||||
'presentation': presentation.id,
|
||||
'facility_limit': limit.id,
|
||||
'movement_date': date(2026, 1, 10),
|
||||
'finance_stage': 'prefi',
|
||||
'currency': trade_finance.currency.id,
|
||||
'signed_financed_amount': Decimal('80000.00'),
|
||||
}])
|
||||
Presentation.post([presentation])
|
||||
Presentation.cancel([presentation])
|
||||
|
||||
presentations = Presentation.search([
|
||||
('trade_finance', '=', trade_finance.id),
|
||||
], order=[('sequence', 'ASC')])
|
||||
self.assertEqual(len(presentations), 2)
|
||||
self.assertEqual(presentations[0].state, 'cancelled')
|
||||
self.assertEqual(presentations[1].state, 'posted')
|
||||
reversal_line, = presentations[1].lines
|
||||
self.assertEqual(reversal_line.signed_financed_amount, Decimal('-80000.00'))
|
||||
|
||||
trade_finance = pool.get('trade.finance')(trade_finance.id)
|
||||
self.assertEqual(trade_finance.exposure_amount, Decimal('0.00'))
|
||||
|
||||
@with_transaction()
|
||||
def test_create_financing_wizard(self):
|
||||
'Test single input financing creation wizard'
|
||||
pool = Pool()
|
||||
TradeFinance = pool.get('trade.finance')
|
||||
Wizard = pool.get('trade.finance.create', type='wizard')
|
||||
|
||||
_, limit, _ = self.create_trade_finance_case()
|
||||
company = create_company(currency=limit.currency)
|
||||
session_id, start_state, _ = Wizard.create()
|
||||
Wizard.execute(session_id, {
|
||||
start_state: {
|
||||
'facility_limit': limit.id,
|
||||
'company': company.id,
|
||||
'bank_reference': 'BANK/TEST/001',
|
||||
'start_date': date(2026, 1, 10),
|
||||
'presentation_date': date(2026, 1, 10),
|
||||
'presentation_type': 'prefi',
|
||||
'finance_stage': 'prefi',
|
||||
'signed_quantity': Decimal('1000.0000'),
|
||||
'commodity_price': Decimal('100.000000'),
|
||||
'signed_financed_amount': Decimal('80000.00'),
|
||||
'post': True,
|
||||
},
|
||||
}, 'create_')
|
||||
|
||||
trade_finance, = TradeFinance.search([
|
||||
('bank_reference', '=', 'BANK/TEST/001'),
|
||||
])
|
||||
presentation, = trade_finance.presentations
|
||||
line, = presentation.lines
|
||||
self.assertEqual(trade_finance.bank, limit.facility.tfe)
|
||||
self.assertEqual(trade_finance.currency, limit.currency)
|
||||
self.assertEqual(presentation.state, 'posted')
|
||||
self.assertEqual(line.facility_limit, limit)
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
||||
Reference in New Issue
Block a user