Add trade finance ledger foundation

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-05-28 22:31:06 +02:00
parent 8542bfeabb
commit 381197b7bb
15 changed files with 3355 additions and 0 deletions

View File

@@ -0,0 +1,414 @@
# Trade Finance Backlog
This backlog tracks the next development phases for the `trade_finance`
module.
Status values:
```text
todo | in-progress | blocked | done
```
Priority values:
```text
low | medium | high
```
## Phase 1 - Core Ledger Foundation
Goal: stabilize the Trade Finance file, presentation, and movement ledger.
### TF-001 - Validate core model installation
Status: todo
Area: test
Priority: high
Goal:
- Install/update the module in a real Tryton database with dependencies.
- Confirm the new models, views, sequence, menus, and access rules load.
Implementation notes:
- Run the targeted module test suite in an environment with `python-sql` and
the full Tryton dependency stack.
- Fix any XML/view/model registration issue found during module activation.
Validation:
- `python -m unittest modules.trade_finance.tests.test_module`
- Manual module activation/update in a development database.
### TF-002 - Harden posted movement immutability
Status: todo
Area: workflow
Priority: high
Goal:
- Ensure posted presentations and posted movement lines cannot be edited or
deleted through direct writes, nested One2Many writes, or UI actions.
Implementation notes:
- Review `write`, `delete`, and nested child update behavior.
- Keep reversal movements as the only correction path for posted exposure.
Validation:
- Add tests for direct line write/delete after posting.
- Add tests for nested line modification through presentation write.
### TF-003 - Add robust exposure helper methods
Status: todo
Area: model
Priority: high
Goal:
- Provide reusable helpers for exposure by facility limit and by Trade Finance
file.
Implementation notes:
- Keep ledger lines as the source of truth.
- Start with Python helpers; SQL/materialized reporting can come later.
- Include `as_of_date` support for historical exposure.
Validation:
- Tests for current exposure by limit.
- Tests for historical exposure before/after transfer movements.
### TF-004 - Improve limit eligibility validation
Status: todo
Area: model
Priority: high
Goal:
- Validate selected `trade_finance.facility_limit` against bank, currency,
validity dates, financing type, and expected operational status.
Implementation notes:
- Reuse `trade_finance.facility_limit_op_status`.
- Do not create a separate `bank.limit` model.
- Consider whether root global limits should be selectable or whether only
leaf sub-limits should receive movements.
Validation:
- Tests for rejected bank mismatch.
- Tests for rejected currency mismatch.
- Tests for rejected financing type mismatch.
- Tests for rejected operational status mismatch once connected.
## Phase 2 - Basic UX and Operational Entry Points
Goal: make the core usable from operational documents.
### TF-005 - Add contextual Start Financing wizard
Status: todo
Area: workflow
Priority: high
Goal:
- Let users start a Trade Finance presentation from an operational document.
Implementation notes:
- Start with stock shipment and stock move because `stock` is already a module
dependency.
- Use `fields.Reference` on presentation origin.
- Pre-fill date, origin, stage, company, currency, and suggested quantity where
available.
Validation:
- Wizard test from `stock.shipment.in`.
- Manual UI check from shipment form.
### TF-006 - Add purchase contract entry points
Status: todo
Area: workflow
Priority: medium
Goal:
- Start financing from purchase contract and purchase contract line when the
relevant purchase contract module is available.
Implementation notes:
- First identify the actual local model names and module dependency impact.
- Avoid adding a hard dependency until confirmed necessary.
- Prefer optional extension module logic if purchase contract is not part of
the base dependency graph.
Validation:
- Tests only if the dependency is activated in the module test environment.
### TF-007 - Add invoice financing entry point
Status: todo
Area: workflow
Priority: medium
Goal:
- Start or continue financing from receivable/invoice stage.
Implementation notes:
- Confirm whether dependency on `account_invoice` is acceptable.
- Origin should point to `account.invoice`.
- Presentation type should default to invoice financing.
Validation:
- Test default origin and stage from invoice when dependency is enabled.
### TF-008 - Add central exposure summary in the UI
Status: todo
Area: view
Priority: medium
Goal:
- Show current exposure by facility limit on the Trade Finance file.
Implementation notes:
- Use a read-only derived view/model or function fields first.
- Do not store manual exposure balances.
Validation:
- Manual UI check with multiple posted movements.
- Tests for summary values if implemented as model logic.
## Phase 3 - Transfer and Lifecycle Workflow
Goal: support movement between financing stages and limits.
### TF-009 - Add transfer proposal helper
Status: todo
Area: workflow
Priority: high
Goal:
- Generate two movement lines for transfers between facility limits.
Implementation notes:
- Negative line on the source facility limit.
- Positive line on the target facility limit.
- Preserve different haircuts/rates on each line.
Validation:
- Test PREFI to IN_TRANSIT transfer with liquidity gain.
### TF-010 - Track liquidity gain/loss
Status: todo
Area: model
Priority: medium
Goal:
- Calculate liquidity gain/loss when moving collateral between limits with
different haircuts.
Implementation notes:
- Keep this derived from movement lines.
- Store applied haircut on each line for audit.
Validation:
- Test gain when target haircut is lower.
- Test loss when target haircut is higher.
### TF-011 - Add repayment/release presentation support
Status: todo
Area: workflow
Priority: medium
Goal:
- Allow users to release financed exposure partially or fully.
Implementation notes:
- Repayment/release should create negative movement lines.
- Avoid editing original posted movements.
Validation:
- Test partial repayment.
- Test full repayment reduces exposure to zero.
## Phase 4 - Funding Cost Engine
Goal: calculate funding costs from chronological exposure movements.
### TF-012 - Build exposure period calculation
Status: todo
Area: model
Priority: high
Goal:
- Convert dated movement lines into running exposure periods per facility
limit.
Implementation notes:
- Sort by movement date.
- Calculate running balance.
- Build intervals until the next movement date or an explicit end date.
Validation:
- Test one movement.
- Test multiple movements on different dates.
- Test same-day movements.
### TF-013 - Calculate interest/funding cost
Status: todo
Area: model
Priority: high
Goal:
- Calculate funding cost using exposure, applied rate, days, and day-count
basis.
Implementation notes:
- Use values copied onto movement lines at posting time.
- Support `360`, `365`, `ACT/360`, and `ACT/365`.
Validation:
- Tests for 360 and 365 bases.
- Test changing facility cost after posting does not alter historical cost.
### TF-014 - Add funding cost report screen
Status: todo
Area: report
Priority: medium
Goal:
- Provide a user-facing report of funding cost by Trade Finance file and by
facility limit.
Implementation notes:
- Start with a Tryton model/report view.
- Add export later only if needed.
Validation:
- Manual report check.
- Tests for report backing model if applicable.
## Phase 5 - Reporting and Controls
Goal: make exposure auditable and operationally controllable.
### TF-015 - Add current exposure by facility limit report
Status: todo
Area: report
Priority: high
Goal:
- Show exposure grouped by `trade_finance.facility_limit`.
Implementation notes:
- Source data must be posted movement lines.
- Include quantity and signed financed amount.
Validation:
- Test aggregation across multiple Trade Finance files.
### TF-016 - Add historical exposure as-of report
Status: todo
Area: report
Priority: medium
Goal:
- Show exposure at a selected historical date.
Implementation notes:
- Filter movement lines where `movement_date <= as_of_date`.
- Group by facility limit and optionally by Trade Finance file.
Validation:
- Tests around before/after transfer dates.
### TF-017 - Add over-limit checks and alerts
Status: todo
Area: workflow
Priority: medium
Goal:
- Detect when posted exposure exceeds facility limit amount.
Implementation notes:
- Decide whether to block posting or warn first.
- Consider parent/global limit and child sub-limit behavior.
Validation:
- Test over child limit.
- Test over global limit if hierarchical aggregation is implemented.
### TF-018 - Add expired limit controls
Status: todo
Area: workflow
Priority: medium
Goal:
- Highlight or block new exposure on expired limits.
Implementation notes:
- Posting already checks movement date validity.
- Add UI filtering and operational alerts.
Validation:
- Manual UI domain check.
- Tests for posting behavior already covered by core validation.
## Phase 6 - Advanced Operational Integration
Goal: connect financing to the broader commodity workflow.
### TF-019 - Add sale matching support
Status: todo
Area: workflow
Priority: medium
Goal:
- Continue financing lifecycle when goods are matched with sales.
Implementation notes:
- Identify the actual local matching model.
- Add it to origin selection only after confirming dependency/model names.
Validation:
- Scenario test once model dependency is clear.
### TF-020 - Add automatic stage suggestions
Status: todo
Area: workflow
Priority: medium
Goal:
- Suggest finance stage from origin document status.
Implementation notes:
- Keep suggestions editable.
- Implement stage inference per origin model.
Validation:
- Tests per supported origin type.
### TF-021 - Add derived snapshot table if performance requires it
Status: todo
Area: report
Priority: low
Goal:
- Improve reporting performance for large ledgers.
Implementation notes:
- Snapshot must be derived from ledger lines.
- Ledger remains the source of truth.
- Do not implement until performance data justifies it.
Validation:
- Reconciliation test comparing snapshot totals with ledger totals.

View File

@@ -0,0 +1,175 @@
# Trade Finance Development Guidance
This file captures implementation guidance for the next development steps of
the `trade_finance` module.
It complements `tryton_trade_finance_architecture.md` and should be used as the
practical working note before creating or changing code.
## 1. Important Existing Model Mapping
The architecture document uses the conceptual name `bank.limit`.
In this codebase, do not create a new `bank.limit` model for the Trade Finance
ledger unless there is a later explicit design decision to do so.
Use the existing facility structure instead:
```text
Concept in architecture doc Existing model in this module
--------------------------- -----------------------------
bank.limit trade_finance.facility_limit
bank / banking facility trade_finance.facility
bank reference / provider trade_finance.facility.tfe
limit amount trade_finance.facility_limit.amount
limit currency trade_finance.facility_limit.currency
limit validity trade_finance.facility_limit.date_from/date_to
limit financing type trade_finance.facility_limit.financing_type
limit haircut rules trade_finance.facility_limit_haircut
limit accepted currencies trade_finance.facility_limit_currency
limit cost/rate rules trade_finance.facility_limit_cost
limit operational eligibility trade_finance.facility_limit_op_status
```
Any future presentation ledger line should therefore reference
`trade_finance.facility_limit`, not a hypothetical `bank.limit`.
## 2. Development Principles
- Keep the existing facility/limit model as the contractual source of truth.
- Treat presentation lines as an exposure movement ledger.
- Store signed amounts and signed quantities on movement lines.
- Do not store or manually update current exposure balances as source data.
- Copy applied haircut/rate/cost values onto posted movement lines for audit.
- Use reversal movements for cancellations instead of editing posted movements.
- Keep contextual origin links polymorphic with `fields.Reference`.
- Add new operational start points by extending the reference selection and
action logic, not by adding many foreign keys to a header model.
## 3. Suggested Documentation Structure
Use a small set of focused markdown files under `modules/trade_finance/documentation/`.
Recommended structure:
```text
documentation/
tryton_trade_finance_architecture.md
development_guidance.md
roadmap.md
backlog.md
bugs.md
decisions.md
test_plan.md
```
Purpose of each file:
```text
tryton_trade_finance_architecture.md
Long-form target architecture and conceptual model.
development_guidance.md
Practical implementation rules, mappings to existing models, and local
conventions for developers.
roadmap.md
Ordered development phases. Stable enough to show where the module is going.
backlog.md
Concrete features/tasks not yet scheduled. One item per small deliverable.
bugs.md
Known defects, reproduction notes, expected behavior, and current status.
decisions.md
Architecture decision records. Use for choices that should not be rediscovered
every time, such as using facility_limit instead of bank.limit.
test_plan.md
Targeted validation strategy, scenarios to cover, and commands to run.
```
Do not create every file upfront if it would remain empty. Create them when
there is useful content to record.
## 4. Suggested Item Formats
### Backlog Item
```text
## TF-001 - Short title
Status: todo | in-progress | blocked | done
Area: model | workflow | view | report | test | data
Priority: low | medium | high
Goal:
- What business behavior should exist?
Implementation notes:
- Smallest expected code change.
- Existing model(s) to reuse.
Validation:
- Target test file or scenario.
- Manual check if needed.
```
### Bug Item
```text
## BUG-001 - Short title
Status: open | reproduced | fixed | closed
Severity: low | medium | high | critical
Area: model | workflow | view | report | test | data
Observed:
- What happens now?
Expected:
- What should happen?
Reproduction:
- Minimal steps or test data.
Fix notes:
- Linked commit/test once fixed.
```
### Decision Record
```text
## ADR-001 - Short title
Date: YYYY-MM-DD
Status: proposed | accepted | superseded
Context:
- Why a decision is needed.
Decision:
- What we decided.
Consequences:
- What this enables.
- What tradeoffs remain.
```
## 5. Immediate Next Development Focus
Recommended next implementation sequence:
1. Create the Trade Finance file/header model.
2. Create the presentation/event model.
3. Create the presentation line ledger model linked to
`trade_finance.facility_limit`.
4. Add sequence/configuration for internal Trade Finance references.
5. Add minimal list/form views and access rules.
6. Add targeted tests around creation, references, signed movements, and limit
validity checks.
The first implementation should stay narrow: no funding-cost engine, no
snapshot table, and no automatic exposure summary as source of truth until the
ledger model is stable.

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ from . import (
operational,
facility,
constraint_type,
trade,
)
@@ -52,7 +53,14 @@ def register():
facility.FacilityCapHaircut,
facility.FacilityConstraint,
constraint_type.ConstraintType,
trade.TradeFinance,
trade.TradeFinancePresentation,
trade.TradeFinancePresentationLine,
trade.TradeFinanceCreateStart,
module='trade_finance', type_='model')
Pool.register(
fx.PriceCalendar,
module='price', type_='model')
Pool.register(
trade.TradeFinanceCreate,
module='trade_finance', type_='wizard')

View File

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

View File

@@ -0,0 +1,854 @@
# 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 decimal import Decimal
from trytond.exceptions import UserError
from trytond.model import ModelSQL, ModelView, Workflow, fields
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.wizard import Button, StateAction, StateView, Wizard
__all__ = [
'TradeFinance',
'TradeFinancePresentation',
'TradeFinancePresentationLine',
'TradeFinanceCreateStart',
'TradeFinanceCreate',
]
_DRAFT_STATES = {
'readonly': Eval('state') != 'draft',
}
_DRAFT_DEPENDS = ['state']
class TradeFinance(Workflow, ModelSQL, ModelView):
'Trade Finance'
__name__ = 'trade.finance'
_rec_name = 'number'
number = fields.Char(
'Trade Finance Reference', required=True, readonly=True)
bank_reference = fields.Char('Bank Reference')
bank = fields.Many2One(
'bank', 'Trade Finance Entity', required=True, ondelete='RESTRICT',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
party = fields.Many2One(
'party.party', 'Counterparty', ondelete='RESTRICT',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
company = fields.Many2One(
'company.company', 'Company', required=True, ondelete='RESTRICT',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
currency = fields.Many2One(
'currency.currency', 'Currency', required=True, ondelete='RESTRICT',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
start_date = fields.Date(
'Start Date', required=True,
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
state = fields.Selection([
('draft', 'Draft'),
('active', 'Active'),
('closed', 'Closed'),
('cancelled', 'Cancelled'),
], 'State', required=True, readonly=True)
description = fields.Text(
'Description', states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
presentations = fields.One2Many(
'trade.finance.presentation', 'trade_finance', 'Presentations',
states={'readonly': Eval('state').in_(['closed', 'cancelled'])},
depends=['state'])
exposure_amount = fields.Function(fields.Numeric(
'Exposure Amount', digits=(16, 2)), 'get_exposure_amount')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('number', 'DESC'))
cls._transitions |= set((
('draft', 'active'),
('active', 'closed'),
('draft', 'cancelled'),
('active', 'cancelled'),
('cancelled', 'draft'),
))
cls._buttons.update({
'activate': {
'invisible': Eval('state') != 'draft',
'depends': ['state'],
},
'close': {
'invisible': Eval('state') != 'active',
'depends': ['state'],
},
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'active']),
'depends': ['state'],
},
'draft': {
'invisible': Eval('state') != 'cancelled',
'depends': ['state'],
},
})
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_start_date():
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_company():
return Transaction().context.get('company')
@classmethod
def _new_number(cls):
pool = Pool()
ModelData = pool.get('ir.model.data')
Sequence = pool.get('ir.sequence')
try:
sequence_id = ModelData.get_id(
'trade_finance', 'sequence_trade_finance')
except KeyError:
return '/'
return Sequence(sequence_id).get()
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
values.setdefault('number', cls._new_number())
return super().create(vlist)
@classmethod
def write(cls, *args):
actions = iter(args)
for records, values in zip(actions, actions):
if set(values) - {'state'}:
for record in records:
if record.state in {'closed', 'cancelled'}:
raise UserError(
'Closed or cancelled trade finance files cannot '
'be modified.')
return super().write(*args)
def get_rec_name(self, name):
parts = [self.number]
if self.bank_reference:
parts.append('[%s]' % self.bank_reference)
return ' '.join(parts)
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
('number', operator, value),
('bank_reference', operator, value),
]
@classmethod
def get_exposure_amount(cls, records, name):
amounts = {r.id: Decimal('0.00') for r in records}
for record in records:
for presentation in record.presentations:
if presentation.state == 'draft':
continue
for line in presentation.lines:
amounts[record.id] += line.signed_financed_amount or 0
return amounts
@classmethod
@ModelView.button
@Workflow.transition('active')
def activate(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('closed')
def close(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
pass
class TradeFinancePresentation(Workflow, ModelSQL, ModelView):
'Trade Finance Presentation'
__name__ = 'trade.finance.presentation'
trade_finance = fields.Many2One(
'trade.finance', 'Trade Finance', required=True, ondelete='CASCADE',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
sequence = fields.Integer(
'Sequence', states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
presentation_date = fields.Date(
'Presentation Date', required=True,
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
presentation_type = fields.Selection([
('prefi', 'Prefinancing'),
('transfer', 'Transfer'),
('shipment', 'Shipment Presentation'),
('matching', 'Matched Sale'),
('invoice', 'Invoice Financing'),
('release', 'Release'),
('cancellation', 'Cancellation'),
], 'Presentation Type', required=True,
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
finance_stage = fields.Selection([
('prefi', 'Prefi'),
('produced', 'Produced'),
('inland_transit', 'Inland Transit'),
('port_stock', 'Port Stock'),
('on_board', 'On Board'),
('ocean_transit', 'Ocean Transit'),
('delivered', 'Delivered'),
('matched', 'Matched'),
('invoiced', 'Invoiced'),
('repaid', 'Repaid'),
('cancelled', 'Cancelled'),
], 'Finance Stage', required=True,
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
financing_type = fields.Many2One(
'trade_finance.financing_type', 'Financing Type',
ondelete='RESTRICT', states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
origin = fields.Reference(
'Origin', selection='get_origin',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
previous_presentation = fields.Many2One(
'trade.finance.presentation', 'Previous Presentation',
ondelete='RESTRICT', states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
description = fields.Text(
'Description', states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
lines = fields.One2Many(
'trade.finance.presentation.line', 'presentation', 'Movement Lines',
states=_DRAFT_STATES, depends=_DRAFT_DEPENDS)
state = fields.Selection([
('draft', 'Draft'),
('posted', 'Posted'),
('cancelled', 'Cancelled'),
], 'State', required=True, readonly=True)
currency = fields.Function(fields.Many2One(
'currency.currency', 'Currency'), 'on_change_with_currency')
bank = fields.Function(fields.Many2One(
'bank', 'Trade Finance Entity'), 'on_change_with_bank')
exposure_amount = fields.Function(fields.Numeric(
'Exposure Amount', digits=(16, 2)), 'get_exposure_amount')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('presentation_date', 'DESC'))
cls._order.insert(1, ('sequence', 'DESC'))
cls._transitions |= set((
('draft', 'posted'),
('posted', 'cancelled'),
))
cls._buttons.update({
'post': {
'invisible': Eval('state') != 'draft',
'depends': ['state'],
},
'cancel': {
'invisible': Eval('state') != 'posted',
'depends': ['state'],
},
})
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_presentation_date():
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_presentation_type():
return 'prefi'
@staticmethod
def default_finance_stage():
return 'prefi'
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference.'
return ['stock.shipment.in', 'stock.shipment.out', 'stock.move']
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
get_name = Model.get_name
return [(None, '')] + [
(model, get_name(model)) for model in cls._get_origin()]
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
if values.get('trade_finance') and not values.get('sequence'):
values['sequence'] = cls._next_sequence(
values['trade_finance'])
return super().create(vlist)
@classmethod
def write(cls, *args):
actions = iter(args)
for records, values in zip(actions, actions):
if set(values) - {'state'}:
for record in records:
if record.state != 'draft':
raise UserError(
'Posted or cancelled presentations cannot be '
'modified.')
return super().write(*args)
@classmethod
def delete(cls, records):
for record in records:
if record.state != 'draft':
raise UserError(
'Posted or cancelled presentations cannot be deleted.')
return super().delete(records)
@classmethod
def _next_sequence(cls, trade_finance):
presentations = cls.search([
('trade_finance', '=', trade_finance),
], order=[('sequence', 'DESC')], limit=1)
if presentations and presentations[0].sequence:
return presentations[0].sequence + 1
return 1
@fields.depends('trade_finance', '_parent_trade_finance.currency')
def on_change_with_currency(self, name=None):
return self.trade_finance.currency if self.trade_finance else None
@fields.depends('trade_finance', '_parent_trade_finance.bank')
def on_change_with_bank(self, name=None):
return self.trade_finance.bank if self.trade_finance else None
def get_rec_name(self, name):
items = []
if self.trade_finance:
items.append(self.trade_finance.number)
if self.sequence:
items.append('#%s' % self.sequence)
items.append(self.presentation_type_string)
return ' '.join(items)
@classmethod
def get_exposure_amount(cls, records, name):
amounts = {r.id: Decimal('0.00') for r in records}
for record in records:
for line in record.lines:
amounts[record.id] += line.signed_financed_amount or 0
return amounts
@classmethod
@ModelView.button
@Workflow.transition('posted')
def post(cls, presentations):
Line = Pool().get('trade.finance.presentation.line')
lines = []
for presentation in presentations:
presentation.check_for_posting()
for line in presentation.lines:
line.apply_limit_values()
line.check_for_posting()
lines.append(line)
if lines:
Line.save(lines)
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, presentations):
for presentation in presentations:
presentation.create_reversal()
def check_for_posting(self):
if not self.lines:
raise UserError(
'A presentation must contain at least one movement line before '
'posting.')
if self.presentation_type == 'transfer' and not self.previous_presentation:
raise UserError(
'Transfer presentations must reference a previous '
'presentation.')
def create_reversal(self):
Date = Pool().get('ir.date')
reversal_lines = []
for line in self.lines:
reversal_lines.append(('create', [line.get_reverse_values()]))
self.__class__.create([{
'trade_finance': self.trade_finance.id,
'sequence': self.__class__._next_sequence(self.trade_finance.id),
'presentation_date': Date.today(),
'presentation_type': 'cancellation',
'finance_stage': 'cancelled',
'financing_type': (
self.financing_type.id if self.financing_type else None),
'previous_presentation': self.id,
'description': 'Reversal of %s' % self.rec_name,
'state': 'posted',
'lines': reversal_lines,
}])
class TradeFinancePresentationLine(ModelSQL, ModelView):
'Trade Finance Presentation Line'
__name__ = 'trade.finance.presentation.line'
presentation = fields.Many2One(
'trade.finance.presentation', 'Presentation',
required=True, ondelete='CASCADE')
facility_limit = fields.Many2One(
'trade_finance.facility_limit', 'Facility Limit', required=True,
ondelete='RESTRICT')
movement_date = fields.Date('Movement Date', required=True)
finance_stage = fields.Selection([
('prefi', 'Prefi'),
('produced', 'Produced'),
('inland_transit', 'Inland Transit'),
('port_stock', 'Port Stock'),
('on_board', 'On Board'),
('ocean_transit', 'Ocean Transit'),
('delivered', 'Delivered'),
('matched', 'Matched'),
('invoiced', 'Invoiced'),
('repaid', 'Repaid'),
('cancelled', 'Cancelled'),
], 'Finance Stage', required=True)
signed_quantity = fields.Numeric('Signed Quantity', digits=(16, 4))
uom = fields.Many2One('product.uom', 'UoM', ondelete='RESTRICT')
commodity_price = fields.Numeric('Commodity Price', digits=(16, 6))
gross_collateral_value = fields.Numeric(
'Gross Collateral Value', digits=(16, 2))
haircut_percent_applied = fields.Numeric(
'Haircut % Applied', digits=(16, 2))
financed_amount = fields.Numeric('Financed Amount', digits=(16, 2))
signed_financed_amount = fields.Numeric(
'Signed Financed Amount', digits=(16, 2), required=True)
interest_rate_applied = fields.Numeric(
'Interest Rate % Applied', digits=(16, 4))
day_count_basis_applied = fields.Selection([
('360', '360'),
('365', '365'),
('act_360', 'ACT/360'),
('act_365', 'ACT/365'),
], 'Day Count Basis')
currency = fields.Many2One(
'currency.currency', 'Currency', required=True, ondelete='RESTRICT')
value_date = fields.Date('Value Date')
maturity_date = fields.Date('Maturity Date')
description = fields.Text('Description')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('movement_date', 'DESC'))
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
cls._set_defaults_from_presentation(values)
cls._set_amounts(values)
return super().create(vlist)
@classmethod
def write(cls, *args):
actions = iter(args)
for lines, values in zip(actions, actions):
if values:
for line in lines:
if line.presentation.state != 'draft':
raise UserError(
'Posted or cancelled movement lines cannot be '
'modified.')
args = list(args)
actions = iter(args)
new_args = []
for lines, values in zip(actions, actions):
values = values.copy()
cls._set_defaults_from_presentation(values)
cls._set_amounts(values)
new_args.extend((lines, values))
return super().write(*new_args)
@classmethod
def delete(cls, lines):
for line in lines:
if line.presentation.state != 'draft':
raise UserError(
'Posted or cancelled movement lines cannot be deleted.')
return super().delete(lines)
@classmethod
def _set_defaults_from_presentation(cls, values):
if not values.get('presentation'):
return
presentation = Pool().get('trade.finance.presentation')(
values['presentation'])
values.setdefault('movement_date', presentation.presentation_date)
values.setdefault('finance_stage', presentation.finance_stage)
if presentation.trade_finance:
values.setdefault('currency', presentation.trade_finance.currency.id)
@classmethod
def _set_amounts(cls, values):
quantity = values.get('signed_quantity')
price = values.get('commodity_price')
haircut = values.get('haircut_percent_applied')
gross = values.get('gross_collateral_value')
financed = values.get('financed_amount')
if gross is None and quantity is not None and price is not None:
gross = abs(quantity) * price
values['gross_collateral_value'] = gross
if financed is None and gross is not None:
haircut = haircut or Decimal('0')
financed = gross * (Decimal('1') - haircut / Decimal('100'))
values['financed_amount'] = financed
if (values.get('signed_financed_amount') is None
and financed is not None):
sign = Decimal('1')
if quantity is not None and quantity < 0:
sign = Decimal('-1')
values['signed_financed_amount'] = financed * sign
@fields.depends('presentation', '_parent_presentation.presentation_date',
'_parent_presentation.finance_stage',
'_parent_presentation.trade_finance',
'_parent_presentation.trade_finance.currency')
def on_change_presentation(self):
if not self.presentation:
return
self.movement_date = self.presentation.presentation_date
self.finance_stage = self.presentation.finance_stage
if self.presentation.trade_finance:
self.currency = self.presentation.trade_finance.currency
@fields.depends('signed_quantity', 'commodity_price')
def on_change_with_gross_collateral_value(self, name=None):
if self.signed_quantity is None or self.commodity_price is None:
return None
return abs(self.signed_quantity) * self.commodity_price
@fields.depends('gross_collateral_value', 'haircut_percent_applied')
def on_change_with_financed_amount(self, name=None):
if self.gross_collateral_value is None:
return None
haircut = self.haircut_percent_applied or Decimal('0')
return self.gross_collateral_value * (
Decimal('1') - haircut / Decimal('100'))
@fields.depends('financed_amount', 'signed_quantity')
def on_change_with_signed_financed_amount(self, name=None):
if self.financed_amount is None:
return None
sign = Decimal('1')
if self.signed_quantity is not None and self.signed_quantity < 0:
sign = Decimal('-1')
return self.financed_amount * sign
def apply_limit_values(self):
haircut_copied = False
if self.haircut_percent_applied is None:
self.haircut_percent_applied = (
self._get_applicable_haircut() or Decimal('0'))
haircut_copied = True
if self.interest_rate_applied is None:
self.interest_rate_applied = (
self._get_applicable_interest_rate() or Decimal('0'))
if not self.day_count_basis_applied:
self.day_count_basis_applied = '360'
if self.gross_collateral_value is None:
self.gross_collateral_value = (
self.on_change_with_gross_collateral_value())
if self.financed_amount is None or haircut_copied:
self.financed_amount = self.on_change_with_financed_amount()
if self.signed_financed_amount is None or (
haircut_copied and self.signed_quantity is not None):
self.signed_financed_amount = (
self.on_change_with_signed_financed_amount())
def check_for_posting(self):
presentation = self.presentation
trade_finance = presentation.trade_finance
limit = self.facility_limit
movement_date = self.movement_date or presentation.presentation_date
if self.signed_financed_amount == 0:
raise UserError('Signed financed amount cannot be zero.')
if limit.facility.tfe != trade_finance.bank:
raise UserError(
"The movement limit must belong to the presentation's Trade "
'Finance Entity.')
if limit.currency != self.currency:
raise UserError(
'The movement currency must match the facility limit currency.')
if movement_date < limit.date_from or movement_date > limit.date_to:
raise UserError(
'The movement date must be within the facility limit validity '
'period.')
if (presentation.financing_type and limit.financing_type
and presentation.financing_type != limit.financing_type):
raise UserError(
'The movement financing type must match the facility limit '
'financing type.')
def _get_applicable_haircut(self):
for haircut in self.facility_limit.haircuts:
if self._date_matches(haircut.date_from, haircut.date_to):
return haircut.haircut_pct
return None
def _get_applicable_interest_rate(self):
for cost in self.facility_limit.costs:
if (cost.cost_type == 'interest'
and self._date_matches(cost.date_from, cost.date_to)):
return cost.spread
return None
def _date_matches(self, date_from, date_to):
date = self.movement_date or self.presentation.presentation_date
if date_from and date < date_from:
return False
if date_to and date > date_to:
return False
return True
def get_reverse_values(self):
return {
'facility_limit': self.facility_limit.id,
'movement_date': Pool().get('ir.date').today(),
'finance_stage': 'cancelled',
'signed_quantity': (
-self.signed_quantity if self.signed_quantity is not None
else None),
'uom': self.uom.id if self.uom else None,
'commodity_price': self.commodity_price,
'gross_collateral_value': self.gross_collateral_value,
'haircut_percent_applied': self.haircut_percent_applied,
'financed_amount': self.financed_amount,
'signed_financed_amount': -self.signed_financed_amount,
'interest_rate_applied': self.interest_rate_applied,
'day_count_basis_applied': self.day_count_basis_applied,
'currency': self.currency.id,
'value_date': self.value_date,
'maturity_date': self.maturity_date,
'description': 'Reversal of %s' % (self.description or self.id),
}
class TradeFinanceCreateStart(ModelView):
'Create Trade Finance Start'
__name__ = 'trade.finance.create.start'
facility_limit = fields.Many2One(
'trade_finance.facility_limit', 'Facility Limit', required=True,
ondelete='RESTRICT')
bank = fields.Function(fields.Many2One(
'bank', 'Trade Finance Entity'), 'on_change_with_bank')
company = fields.Many2One(
'company.company', 'Company', required=True, ondelete='RESTRICT')
party = fields.Many2One(
'party.party', 'Counterparty', ondelete='RESTRICT')
currency = fields.Function(fields.Many2One(
'currency.currency', 'Currency'), 'on_change_with_currency')
bank_reference = fields.Char('Bank Reference')
start_date = fields.Date('Start Date', required=True)
presentation_date = fields.Date('Presentation Date', required=True)
presentation_type = fields.Selection([
('prefi', 'Prefinancing'),
('transfer', 'Transfer'),
('shipment', 'Shipment Presentation'),
('matching', 'Matched Sale'),
('invoice', 'Invoice Financing'),
('release', 'Release'),
('cancellation', 'Cancellation'),
], 'Presentation Type', required=True)
finance_stage = fields.Selection([
('prefi', 'Prefi'),
('produced', 'Produced'),
('inland_transit', 'Inland Transit'),
('port_stock', 'Port Stock'),
('on_board', 'On Board'),
('ocean_transit', 'Ocean Transit'),
('delivered', 'Delivered'),
('matched', 'Matched'),
('invoiced', 'Invoiced'),
('repaid', 'Repaid'),
('cancelled', 'Cancelled'),
], 'Finance Stage', required=True)
financing_type = fields.Function(fields.Many2One(
'trade_finance.financing_type', 'Financing Type'),
'on_change_with_financing_type')
signed_quantity = fields.Numeric('Signed Quantity', digits=(16, 4))
uom = fields.Many2One('product.uom', 'UoM', ondelete='RESTRICT')
commodity_price = fields.Numeric('Commodity Price', digits=(16, 6))
gross_collateral_value = fields.Numeric(
'Gross Collateral Value', digits=(16, 2))
haircut_percent_applied = fields.Numeric(
'Haircut % Applied', digits=(16, 2))
financed_amount = fields.Numeric('Financed Amount', digits=(16, 2))
signed_financed_amount = fields.Numeric(
'Signed Financed Amount', digits=(16, 2), required=True)
value_date = fields.Date('Value Date')
maturity_date = fields.Date('Maturity Date')
description = fields.Text('Description')
post = fields.Boolean('Post Presentation')
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_start_date():
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_presentation_date():
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_presentation_type():
return 'prefi'
@staticmethod
def default_finance_stage():
return 'prefi'
@staticmethod
def default_post():
return False
@fields.depends('facility_limit', '_parent_facility_limit.facility')
def on_change_with_bank(self, name=None):
if self.facility_limit and self.facility_limit.facility:
return self.facility_limit.facility.tfe
@fields.depends('facility_limit', '_parent_facility_limit.currency')
def on_change_with_currency(self, name=None):
if self.facility_limit:
return self.facility_limit.currency
@fields.depends('facility_limit', '_parent_facility_limit.financing_type')
def on_change_with_financing_type(self, name=None):
if self.facility_limit:
return self.facility_limit.financing_type
@fields.depends('signed_quantity', 'commodity_price')
def on_change_with_gross_collateral_value(self, name=None):
if self.signed_quantity is None or self.commodity_price is None:
return None
return abs(self.signed_quantity) * self.commodity_price
@fields.depends('gross_collateral_value', 'haircut_percent_applied')
def on_change_with_financed_amount(self, name=None):
if self.gross_collateral_value is None:
return None
haircut = self.haircut_percent_applied or Decimal('0')
return self.gross_collateral_value * (
Decimal('1') - haircut / Decimal('100'))
@fields.depends('financed_amount', 'signed_quantity')
def on_change_with_signed_financed_amount(self, name=None):
if self.financed_amount is None:
return None
sign = Decimal('1')
if self.signed_quantity is not None and self.signed_quantity < 0:
sign = Decimal('-1')
return self.financed_amount * sign
class TradeFinanceCreate(Wizard):
'Create Trade Finance'
__name__ = 'trade.finance.create'
start = StateView('trade.finance.create.start',
'trade_finance.trade_finance_create_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'create_', 'tryton-ok', default=True),
])
create_ = StateAction('trade_finance.act_trade_finance')
def do_create_(self, action):
pool = Pool()
TradeFinance = pool.get('trade.finance')
Presentation = pool.get('trade.finance.presentation')
Line = pool.get('trade.finance.presentation.line')
start = self.start
facility_limit = start.facility_limit
bank = facility_limit.facility.tfe
currency = facility_limit.currency
financing_type = facility_limit.financing_type
trade_finance, = TradeFinance.create([{
'bank_reference': start.bank_reference,
'bank': bank.id,
'party': start.party.id if start.party else None,
'company': start.company.id,
'currency': currency.id,
'start_date': start.start_date,
'description': start.description,
}])
presentation, = Presentation.create([{
'trade_finance': trade_finance.id,
'presentation_date': start.presentation_date,
'presentation_type': start.presentation_type,
'finance_stage': start.finance_stage,
'financing_type': financing_type.id if financing_type else None,
'description': start.description,
}])
line, = Line.create([{
'presentation': presentation.id,
'facility_limit': facility_limit.id,
'movement_date': start.presentation_date,
'finance_stage': start.finance_stage,
'signed_quantity': start.signed_quantity,
'uom': start.uom.id if start.uom else None,
'commodity_price': start.commodity_price,
'gross_collateral_value': start.gross_collateral_value,
'haircut_percent_applied': start.haircut_percent_applied,
'financed_amount': start.financed_amount,
'signed_financed_amount': start.signed_financed_amount,
'currency': currency.id,
'value_date': start.value_date,
'maturity_date': start.maturity_date,
'description': start.description,
}])
if start.post:
Presentation.post([presentation])
else:
line.apply_limit_values()
Line.save([line])
action['res_id'] = [trade_finance.id]
action['views'].reverse()
return action, {}

View File

@@ -0,0 +1,254 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- ================================================================ -->
<!-- SEQUENCE -->
<!-- ================================================================ -->
<record model="ir.sequence.type" id="sequence_type_trade_finance">
<field name="name">Trade Finance</field>
</record>
<record model="ir.sequence.type-res.group"
id="sequence_type_trade_finance_group_admin">
<field name="sequence_type" ref="sequence_type_trade_finance"/>
<field name="group" ref="group_trade_finance_admin"/>
</record>
<record model="ir.sequence" id="sequence_trade_finance">
<field name="name">Trade Finance</field>
<field name="sequence_type" ref="sequence_type_trade_finance"/>
<field name="prefix">TFR-</field>
<field name="padding" eval="4"/>
</record>
<!-- ================================================================ -->
<!-- SINGLE INPUT CREATION WIZARD -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="trade_finance_create_start_view_form">
<field name="model">trade.finance.create.start</field>
<field name="type">form</field>
<field name="name">trade_finance_create_start_form</field>
</record>
<record model="ir.action.wizard" id="wizard_trade_finance_create">
<field name="name">New Financing</field>
<field name="wiz_name">trade.finance.create</field>
</record>
<menuitem
name="New Financing"
sequence="1"
id="menu_trade_finance_create"
parent="menu_trade_finance"
action="wizard_trade_finance_create"/>
<!-- ================================================================ -->
<!-- TRADE FINANCE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="trade_finance_view_tree">
<field name="model">trade.finance</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">trade_finance_tree</field>
</record>
<record model="ir.ui.view" id="trade_finance_view_form">
<field name="model">trade.finance</field>
<field name="type">form</field>
<field name="name">trade_finance_form</field>
</record>
<record model="ir.action.act_window" id="act_trade_finance">
<field name="name">Trade Finance Files</field>
<field name="res_model">trade.finance</field>
</record>
<record model="ir.action.act_window.view" id="act_trade_finance_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="trade_finance_view_tree"/>
<field name="act_window" ref="act_trade_finance"/>
</record>
<record model="ir.action.act_window.view" id="act_trade_finance_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="trade_finance_view_form"/>
<field name="act_window" ref="act_trade_finance"/>
</record>
<record model="ir.action.act_window.domain"
id="act_trade_finance_domain_draft">
<field name="name">Draft</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('state', '=', 'draft')]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_trade_finance"/>
</record>
<record model="ir.action.act_window.domain"
id="act_trade_finance_domain_active">
<field name="name">Active</field>
<field name="sequence" eval="20"/>
<field name="domain" eval="[('state', '=', 'active')]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_trade_finance"/>
</record>
<record model="ir.action.act_window.domain"
id="act_trade_finance_domain_all">
<field name="name">All</field>
<field name="sequence" eval="9999"/>
<field name="domain"></field>
<field name="act_window" ref="act_trade_finance"/>
</record>
<menuitem
name="Trade Finance Files"
sequence="5"
id="menu_trade_finance_files"
parent="menu_trade_finance"
action="act_trade_finance"/>
<record model="ir.model.access" id="access_trade_finance">
<field name="model">trade.finance</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_trade_finance_user">
<field name="model">trade.finance</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_trade_finance_admin">
<field name="model">trade.finance</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- PRESENTATION -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="trade_finance_presentation_view_tree">
<field name="model">trade.finance.presentation</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">trade_finance_presentation_tree</field>
</record>
<record model="ir.ui.view" id="trade_finance_presentation_view_form">
<field name="model">trade.finance.presentation</field>
<field name="type">form</field>
<field name="name">trade_finance_presentation_form</field>
</record>
<record model="ir.action.act_window" id="act_trade_finance_presentation">
<field name="name">Trade Finance Presentations</field>
<field name="res_model">trade.finance.presentation</field>
</record>
<record model="ir.action.act_window.view"
id="act_trade_finance_presentation_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="trade_finance_presentation_view_tree"/>
<field name="act_window" ref="act_trade_finance_presentation"/>
</record>
<record model="ir.action.act_window.view"
id="act_trade_finance_presentation_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="trade_finance_presentation_view_form"/>
<field name="act_window" ref="act_trade_finance_presentation"/>
</record>
<menuitem
name="Presentations"
sequence="15"
id="menu_trade_finance_presentations"
parent="menu_trade_finance"
action="act_trade_finance_presentation"/>
<record model="ir.model.access" id="access_trade_finance_presentation">
<field name="model">trade.finance.presentation</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_trade_finance_presentation_user">
<field name="model">trade.finance.presentation</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_trade_finance_presentation_admin">
<field name="model">trade.finance.presentation</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- PRESENTATION LINE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="trade_finance_presentation_line_view_tree">
<field name="model">trade.finance.presentation.line</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">trade_finance_presentation_line_tree</field>
</record>
<record model="ir.ui.view" id="trade_finance_presentation_line_view_form">
<field name="model">trade.finance.presentation.line</field>
<field name="type">form</field>
<field name="name">trade_finance_presentation_line_form</field>
</record>
<record model="ir.action.act_window" id="act_trade_finance_presentation_line">
<field name="name">Trade Finance Movement Lines</field>
<field name="res_model">trade.finance.presentation.line</field>
</record>
<record model="ir.action.act_window.view"
id="act_trade_finance_presentation_line_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="trade_finance_presentation_line_view_tree"/>
<field name="act_window" ref="act_trade_finance_presentation_line"/>
</record>
<record model="ir.action.act_window.view"
id="act_trade_finance_presentation_line_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="trade_finance_presentation_line_view_form"/>
<field name="act_window" ref="act_trade_finance_presentation_line"/>
</record>
<menuitem
name="Movement Lines"
sequence="20"
id="menu_trade_finance_presentation_lines"
parent="menu_trade_finance"
action="act_trade_finance_presentation_line"/>
<record model="ir.model.access" id="access_trade_finance_presentation_line">
<field name="model">trade.finance.presentation.line</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access"
id="access_trade_finance_presentation_line_user">
<field name="model">trade.finance.presentation.line</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access"
id="access_trade_finance_presentation_line_admin">
<field name="model">trade.finance.presentation.line</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
</data>
</tryton>

View File

@@ -10,3 +10,4 @@ xml:
reference.xml
facility.xml
constraint_type.xml
trade.xml

View File

@@ -0,0 +1,54 @@
<form col="6">
<group id="facility" col="6" colspan="6">
<label name="facility_limit"/>
<field name="facility_limit" colspan="3"/>
<label name="post"/>
<field name="post"/>
<label name="bank"/>
<field name="bank" colspan="3"/>
<label name="currency"/>
<field name="currency"/>
<label name="financing_type"/>
<field name="financing_type"/>
</group>
<group id="file" col="6" colspan="6">
<label name="company"/>
<field name="company" colspan="3"/>
<label name="start_date"/>
<field name="start_date"/>
<label name="party"/>
<field name="party" colspan="3"/>
<label name="bank_reference"/>
<field name="bank_reference"/>
</group>
<group id="presentation" col="6" colspan="6">
<label name="presentation_date"/>
<field name="presentation_date"/>
<label name="presentation_type"/>
<field name="presentation_type"/>
<label name="finance_stage"/>
<field name="finance_stage"/>
</group>
<group id="amounts" col="6" colspan="6">
<label name="signed_quantity"/>
<field name="signed_quantity"/>
<label name="uom"/>
<field name="uom"/>
<label name="commodity_price"/>
<field name="commodity_price"/>
<label name="gross_collateral_value"/>
<field name="gross_collateral_value"/>
<label name="haircut_percent_applied"/>
<field name="haircut_percent_applied"/>
<label name="financed_amount"/>
<field name="financed_amount"/>
<label name="signed_financed_amount"/>
<field name="signed_financed_amount"/>
<label name="value_date"/>
<field name="value_date"/>
<label name="maturity_date"/>
<field name="maturity_date"/>
</group>
<label name="description"/>
<field name="description" colspan="5"/>
</form>

View File

@@ -0,0 +1,36 @@
<form col="6">
<group id="header" col="6" colspan="6">
<label name="number"/>
<field name="number"/>
<label name="bank_reference"/>
<field name="bank_reference"/>
<label name="state"/>
<field name="state"/>
<label name="bank"/>
<field name="bank" colspan="3"/>
<label name="start_date"/>
<field name="start_date"/>
<label name="party"/>
<field name="party" colspan="3"/>
<label name="currency"/>
<field name="currency"/>
<label name="company"/>
<field name="company" colspan="3"/>
<label name="exposure_amount"/>
<field name="exposure_amount"/>
<label name="description"/>
<field name="description" colspan="5"/>
</group>
<notebook colspan="6">
<page string="Presentations" id="presentations">
<field name="presentations" colspan="6"
view_ids="trade_finance.trade_finance_presentation_view_tree,trade_finance.trade_finance_presentation_view_form"/>
</page>
</notebook>
<group id="buttons" col="4" colspan="6">
<button name="activate" icon="tryton-forward"/>
<button name="close" icon="tryton-ok"/>
<button name="cancel" icon="tryton-cancel"/>
<button name="draft" icon="tryton-back"/>
</group>
</form>

View File

@@ -0,0 +1,36 @@
<form col="6">
<group id="header" col="6" colspan="6">
<label name="trade_finance"/>
<field name="trade_finance" colspan="3"/>
<label name="state"/>
<field name="state"/>
<label name="sequence"/>
<field name="sequence"/>
<label name="presentation_date"/>
<field name="presentation_date"/>
<label name="presentation_type"/>
<field name="presentation_type"/>
<label name="finance_stage"/>
<field name="finance_stage"/>
<label name="financing_type"/>
<field name="financing_type"/>
<label name="origin"/>
<field name="origin" colspan="3"/>
<label name="previous_presentation"/>
<field name="previous_presentation"/>
<label name="exposure_amount"/>
<field name="exposure_amount"/>
<label name="description"/>
<field name="description" colspan="5"/>
</group>
<notebook colspan="6">
<page string="Movement Lines" id="lines">
<field name="lines" colspan="6"
view_ids="trade_finance.trade_finance_presentation_line_view_tree,trade_finance.trade_finance_presentation_line_view_form"/>
</page>
</notebook>
<group id="buttons" col="2" colspan="6">
<button name="post" icon="tryton-ok"/>
<button name="cancel" icon="tryton-cancel"/>
</group>
</form>

View File

@@ -0,0 +1,36 @@
<form col="6">
<label name="presentation"/>
<field name="presentation" colspan="3"/>
<label name="facility_limit"/>
<field name="facility_limit"/>
<label name="movement_date"/>
<field name="movement_date"/>
<label name="finance_stage"/>
<field name="finance_stage"/>
<label name="currency"/>
<field name="currency"/>
<label name="signed_quantity"/>
<field name="signed_quantity"/>
<label name="uom"/>
<field name="uom"/>
<label name="commodity_price"/>
<field name="commodity_price"/>
<label name="gross_collateral_value"/>
<field name="gross_collateral_value"/>
<label name="haircut_percent_applied"/>
<field name="haircut_percent_applied"/>
<label name="financed_amount"/>
<field name="financed_amount"/>
<label name="signed_financed_amount"/>
<field name="signed_financed_amount"/>
<label name="interest_rate_applied"/>
<field name="interest_rate_applied"/>
<label name="day_count_basis_applied"/>
<field name="day_count_basis_applied"/>
<label name="value_date"/>
<field name="value_date"/>
<label name="maturity_date"/>
<field name="maturity_date"/>
<label name="description"/>
<field name="description" colspan="5"/>
</form>

View File

@@ -0,0 +1,14 @@
<tree>
<field name="presentation"/>
<field name="facility_limit"/>
<field name="movement_date"/>
<field name="finance_stage"/>
<field name="signed_quantity"/>
<field name="uom"/>
<field name="gross_collateral_value"/>
<field name="haircut_percent_applied"/>
<field name="financed_amount"/>
<field name="signed_financed_amount"/>
<field name="interest_rate_applied"/>
<field name="currency"/>
</tree>

View File

@@ -0,0 +1,11 @@
<tree>
<field name="trade_finance"/>
<field name="sequence"/>
<field name="presentation_date"/>
<field name="presentation_type"/>
<field name="finance_stage"/>
<field name="financing_type"/>
<field name="origin"/>
<field name="exposure_amount"/>
<field name="state"/>
</tree>

View File

@@ -0,0 +1,11 @@
<tree>
<field name="number"/>
<field name="bank_reference"/>
<field name="bank"/>
<field name="party"/>
<field name="company"/>
<field name="currency"/>
<field name="start_date"/>
<field name="exposure_amount"/>
<field name="state"/>
</tree>