# This file is part of Tradon. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. 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 class TradeFinanceTestCase(ModuleTestCase): 'Test Trade Finance module' module = 'trade_finance' def create_facility(self): pool = Pool() Bank = pool.get('bank') Facility = pool.get('trade_finance.facility') Party = pool.get('party.party') currency = create_currency('USD') party = Party(name='Test Bank') party.save() bank = Bank(party=party) bank.save() facility = Facility( name='Test Facility', tfe=bank, currency=currency, commitment_status='committed', date_from=date(2026, 1, 1), date_to=date(2026, 12, 31)) 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' pool = Pool() Limit = pool.get('trade_finance.facility_limit') facility, currency = self.create_facility() root, = Limit.create([{ 'facility': facility.id, 'name': 'Global limit', 'currency': currency.id, 'amount': Decimal('100.00'), 'date_from': date(2026, 1, 1), 'date_to': date(2026, 12, 31), }]) child, = Limit.create([{ 'parent': root.id, 'name': 'Trading limit', 'amount': Decimal('50.00'), }]) self.assertEqual(child.facility, facility) self.assertEqual(child.currency, currency) self.assertEqual(child.date_from, root.date_from) self.assertEqual(child.date_to, root.date_to) @with_transaction() def test_facility_limits_only_returns_root_limits(self): 'Test facility limits exclude nested sublimits' pool = Pool() Facility = pool.get('trade_finance.facility') Limit = pool.get('trade_finance.facility_limit') facility, currency = self.create_facility() root, = Limit.create([{ 'facility': facility.id, 'name': 'Global limit', 'currency': currency.id, 'amount': Decimal('100.00'), 'date_from': date(2026, 1, 1), 'date_to': date(2026, 12, 31), }]) child, = Limit.create([{ 'parent': root.id, 'name': 'Trading limit', 'amount': Decimal('50.00'), }]) facility = Facility(facility.id) root = Limit(root.id) self.assertEqual(list(facility.limits), [root]) self.assertEqual(list(root.children), [child]) @with_transaction() def test_sublimit_dates_must_stay_within_parent(self): 'Test sublimit dates stay within parent dates' pool = Pool() Limit = pool.get('trade_finance.facility_limit') facility, currency = self.create_facility() root, = Limit.create([{ 'facility': facility.id, 'name': 'Global limit', 'currency': currency.id, 'amount': Decimal('100.00'), 'date_from': date(2026, 1, 1), 'date_to': date(2026, 12, 31), }]) with self.assertRaises(UserError): Limit.create([{ 'parent': root.id, 'name': 'Late limit', 'amount': Decimal('50.00'), 'date_from': date(2026, 1, 1), 'date_to': date(2027, 1, 1), }]) @with_transaction() def test_currency_change_cascades_to_sublimits(self): 'Test root currency changes cascade to sublimits' pool = Pool() Limit = pool.get('trade_finance.facility_limit') facility, currency = self.create_facility() other_currency = create_currency('EUR') root, = Limit.create([{ 'facility': facility.id, 'name': 'Global limit', 'currency': currency.id, 'amount': Decimal('100.00'), 'date_from': date(2026, 1, 1), 'date_to': date(2026, 12, 31), }]) child, = Limit.create([{ 'parent': root.id, 'name': 'Trading limit', 'amount': Decimal('50.00'), }]) Limit.write([root], {'currency': other_currency.id}) child = Limit(child.id) 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