855 lines
32 KiB
Python
855 lines
32 KiB
Python
# 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, {}
|