Add trade finance ledger foundation
This commit is contained in:
414
modules/trade_finance/Documentation/backlog.md
Normal file
414
modules/trade_finance/Documentation/backlog.md
Normal 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.
|
||||
175
modules/trade_finance/Documentation/development_guidance.md
Normal file
175
modules/trade_finance/Documentation/development_guidance.md
Normal 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
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
854
modules/trade_finance/trade.py
Normal file
854
modules/trade_finance/trade.py
Normal 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, {}
|
||||
254
modules/trade_finance/trade.xml
Normal file
254
modules/trade_finance/trade.xml
Normal 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>
|
||||
@@ -10,3 +10,4 @@ xml:
|
||||
reference.xml
|
||||
facility.xml
|
||||
constraint_type.xml
|
||||
trade.xml
|
||||
|
||||
@@ -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>
|
||||
36
modules/trade_finance/view/trade_finance_form.xml
Normal file
36
modules/trade_finance/view/trade_finance_form.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
11
modules/trade_finance/view/trade_finance_tree.xml
Normal file
11
modules/trade_finance/view/trade_finance_tree.xml
Normal 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>
|
||||
Reference in New Issue
Block a user