Trade Finance + Other changes

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-05-25 11:17:18 +02:00
parent 452e9e2632
commit 181de17b89
13 changed files with 1952 additions and 8 deletions

View File

@@ -1,7 +1,7 @@
# 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 trytond.model import ModelSQL, ModelView, fields
from trytond.model import ModelSQL, ModelView, fields, tree
from trytond.model import sequence_ordered
from trytond.pool import Pool
from trytond.pyson import Eval, Bool
@@ -184,7 +184,7 @@ class FacilityCovenant(ModelSQL, ModelView):
# Facility Limit (Global Limit + Sub-Limits in one model)
# ---------------------------------------------------------------------------
class FacilityLimit(ModelSQL, ModelView):
class FacilityLimit(tree(), ModelSQL, ModelView):
'Facility Limit'
__name__ = 'trade_finance.facility_limit'
_rec_name = 'name'
@@ -194,24 +194,41 @@ class FacilityLimit(ModelSQL, ModelView):
states={'readonly': Bool(Eval('parent'))},
depends=['parent'])
parent = fields.Many2One('trade_finance.facility_limit', 'Parent Limit',
left='left', right='right',
ondelete='RESTRICT',
domain=[('facility', '=', Eval('facility'))],
depends=['facility'],
help='Leave empty for Global Limit (root node)')
children = fields.One2Many('trade_finance.facility_limit', 'parent',
'Sub-Limits')
left = fields.Integer('Left', required=True)
right = fields.Integer('Right', required=True)
name = fields.Char('Name', required=True)
alternative_name = fields.Char('Limit Alternative Name')
financing_type = fields.Many2One('trade_finance.financing_type',
'Financing Type', ondelete='RESTRICT')
currency = fields.Many2One('currency.currency', 'Currency',
ondelete='RESTRICT',
states={'readonly': Bool(Eval('parent'))},
depends=['parent'],
help='Currency of the global limit. Sub-limits inherit it from '
'their parent.')
amount = fields.Numeric('Amount', digits=(16, 2), required=True)
tenor = fields.Integer('Tenor (days)',
help='Maximum duration of financing from drawdown to repayment')
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
sequence = fields.Integer('Sequence')
notes = fields.Text('Notes')
_order = [('sequence', 'ASC'), ('id', 'ASC')]
@classmethod
def __register__(cls, module_name):
super().__register__(module_name)
cls._rebuild_tree('parent', None, 0)
haircuts = fields.One2Many('trade_finance.facility_limit_haircut', 'limit',
'Haircuts')
currencies = fields.One2Many('trade_finance.facility_limit_currency',
@@ -230,6 +247,38 @@ class FacilityLimit(ModelSQL, ModelView):
def default_sequence():
return 10
@staticmethod
def default_left():
return 0
@staticmethod
def default_right():
return 0
@fields.depends('facility', 'currency', 'date_from', 'date_to', 'parent',
'_parent_parent.facility', '_parent_parent.currency',
'_parent_parent.date_from', '_parent_parent.date_to')
def on_change_parent(self):
if self.parent:
self.facility = self.parent.facility
self.currency = self.parent.currency
if not self.date_from:
self.date_from = self.parent.date_from
if not self.date_to:
self.date_to = self.parent.date_to
@fields.depends('facility', 'currency', 'date_from', 'date_to', 'parent',
'_parent_facility.currency', '_parent_facility.date_from',
'_parent_facility.date_to')
def on_change_facility(self):
if self.facility and not self.parent:
if not self.currency:
self.currency = self.facility.currency
if not self.date_from:
self.date_from = self.facility.date_from
if not self.date_to:
self.date_to = self.facility.date_to
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
@@ -237,13 +286,73 @@ class FacilityLimit(ModelSQL, ModelView):
if values.get('parent') and not values.get('facility'):
parent = cls(values['parent'])
values['facility'] = parent.facility.id
if values.get('parent'):
parent = cls(values['parent'])
values['currency'] = (
parent.currency.id if parent.currency else None)
values.setdefault('date_from', parent.date_from)
values.setdefault('date_to', parent.date_to)
elif values.get('facility'):
facility = Pool().get('trade_finance.facility')(
values['facility'])
values.setdefault('currency',
facility.currency.id if facility.currency else None)
values.setdefault('date_from', facility.date_from)
values.setdefault('date_to', facility.date_to)
return super().create(vlist)
@classmethod
def write(cls, *args):
args = list(args)
actions = iter(args)
new_args = []
check_descendants = []
sync_currency = []
for limits, values in zip(actions, actions):
values = values.copy()
if values.get('parent'):
parent = cls(values['parent'])
values['facility'] = parent.facility.id
values['currency'] = (
parent.currency.id if parent.currency else None)
if {'parent', 'currency'} & set(values):
sync_currency.extend(limits)
if {'parent', 'currency', 'date_from', 'date_to'} & set(values):
check_descendants.extend(limits)
new_args.extend((limits, values))
result = super().write(*new_args)
cls._sync_descendant_currencies(sync_currency)
cls._check_descendants(check_descendants)
return result
@classmethod
def _sync_descendant_currencies(cls, limits):
for limit in limits:
limit = cls(limit.id)
if not limit.currency:
continue
descendants = cls.search([('parent', 'child_of', [limit.id])])
if descendants:
super(FacilityLimit, cls).write(
descendants, {'currency': limit.currency.id})
@classmethod
def _check_descendants(cls, limits):
descendants = []
for limit in limits:
descendants.extend(
cls.search([('parent', 'child_of', [limit.id])]))
if descendants:
cls.validate(descendants)
@classmethod
def validate(cls, limits):
super().validate(limits)
for limit in limits:
limit.check_single_root()
limit.check_required_values()
limit.check_parent_consistency()
limit.check_dates_within_parent()
limit.check_amount_vs_parent()
limit.check_children_amounts()
@@ -260,6 +369,49 @@ class FacilityLimit(ModelSQL, ModelView):
f"Limit ('{roots[0].name}'). Only one root limit is "
f"allowed per facility.")
def check_required_values(self):
if not self.currency:
raise UserError(
f"Limit '{self.name}' must define or inherit a currency.")
if not self.date_from or not self.date_to:
raise UserError(
f"Limit '{self.name}' must define valid from and valid to "
f"dates.")
if self.date_from > self.date_to:
raise UserError(
f"Limit '{self.name}' valid from date cannot be after "
f"its valid to date.")
def check_parent_consistency(self):
if not self.parent:
return
if self.facility != self.parent.facility:
raise UserError(
f"Sub-limit '{self.name}' must belong to the same facility "
f"as parent limit '{self.parent.name}'.")
if self.currency != self.parent.currency:
raise UserError(
f"Sub-limit '{self.name}' must use the same currency as "
f"parent limit '{self.parent.name}'.")
def check_dates_within_parent(self):
if self.date_from and self.date_to and self.date_from > self.date_to:
raise UserError(
f"Limit '{self.name}' valid from date cannot be after its "
f"valid to date.")
if not self.parent:
return
if (self.date_from and self.parent.date_from
and self.date_from < self.parent.date_from):
raise UserError(
f"Sub-limit '{self.name}' valid from date cannot be before "
f"parent limit '{self.parent.name}' valid from date.")
if (self.date_to and self.parent.date_to
and self.date_to > self.parent.date_to):
raise UserError(
f"Sub-limit '{self.name}' valid to date cannot be after "
f"parent limit '{self.parent.name}' valid to date.")
def check_amount_vs_parent(self):
if self.parent and self.amount > self.parent.amount:
raise UserError(

View File

@@ -0,0 +1,2 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

View File

@@ -0,0 +1,117 @@
# 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.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
@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_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)
del ModuleTestCase

View File

@@ -30,9 +30,10 @@
<field name="description" colspan="5"/>
</group>
<notebook colspan="6">
<page string="Limits" id="limits">
<page string="Global Limit" id="limits">
<field name="limits" colspan="6"
domain="[('parent', '=', None)]"/>
domain="[('parent', '=', None)]"
view_ids="trade_finance.facility_limit_view_tree,trade_finance.facility_limit_view_form"/>
</page>
<page string="Currencies" id="currencies">
<field name="currencies" colspan="6"/>

View File

@@ -6,8 +6,14 @@
<field name="alternative_name" colspan="3"/>
<label name="financing_type"/>
<field name="financing_type" colspan="3"/>
<label name="currency"/>
<field name="currency"/>
<label name="amount"/>
<field name="amount"/>
<label name="date_from"/>
<field name="date_from"/>
<label name="date_to"/>
<field name="date_to"/>
<label name="tenor"/>
<field name="tenor"/>
<label name="sequence"/>
@@ -17,10 +23,13 @@
<field name="parent" colspan="3"/>
<label name="facility"/>
<field name="facility" colspan="3"/>
<label name="notes"/>
<field name="notes" colspan="3"/>
</group>
<notebook colspan="4">
<page string="Sub-Limits" id="children">
<field name="children" colspan="4"/>
<field name="children" colspan="4"
view_ids="trade_finance.facility_limit_view_tree,trade_finance.facility_limit_view_form"/>
</page>
<page string="Haircuts" id="haircuts">
<field name="haircuts" colspan="4"/>

View File

@@ -1,7 +1,11 @@
<tree expand="1">
<field name="name"/>
<field name="parent"/>
<tree expand="1" keyword_open="1">
<field name="name" expand="2"/>
<field name="parent" tree_invisible="1"/>
<field name="children" tree_invisible="1"/>
<field name="financing_type"/>
<field name="currency"/>
<field name="amount"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="tenor"/>
</tree>