Scenario Mtm
This commit is contained in:
@@ -56,10 +56,21 @@ DAYS = [
|
||||
('thursday', 'Thursday'),
|
||||
('friday', 'Friday'),
|
||||
('saturday', 'Saturday'),
|
||||
('sunday', 'Sunday'),
|
||||
]
|
||||
|
||||
class Estimated(ModelSQL, ModelView):
|
||||
('sunday', 'Sunday'),
|
||||
]
|
||||
|
||||
MTM_VALUATION_DATE_TYPES = [
|
||||
('fixed', 'Fixed Date'),
|
||||
('today', 'Today'),
|
||||
('month_start', 'Start of Month'),
|
||||
('month_end', 'End of Month'),
|
||||
('previous_month_end', 'End of Previous Month'),
|
||||
('next_month_end', 'End of Next Month'),
|
||||
('year_start', 'Start of Year'),
|
||||
('year_end', 'End of Year'),
|
||||
]
|
||||
|
||||
class Estimated(ModelSQL, ModelView):
|
||||
"Estimated date"
|
||||
__name__ = 'pricing.estimated'
|
||||
|
||||
@@ -558,18 +569,64 @@ class ImportPrices(Wizard):
|
||||
if re.match(r'^\d+(\.\d+)?$', text):
|
||||
return cls._as_date(text).strftime('%Y-%m')
|
||||
raise UserError("Invalid month term: %s" % text)
|
||||
class MtmScenario(ModelSQL, ModelView):
|
||||
"MtM Scenario"
|
||||
__name__ = 'mtm.scenario'
|
||||
|
||||
name = fields.Char("Scenario", required=True)
|
||||
valuation_date = fields.Date("Valuation Date", required=True)
|
||||
use_last_price = fields.Boolean("Use Last Available Price")
|
||||
calendar = fields.Many2One(
|
||||
'price.calendar', "Calendar"
|
||||
)
|
||||
|
||||
class MtmStrategy(ModelSQL, ModelView):
|
||||
class MtmScenario(ModelSQL, ModelView):
|
||||
"MtM Scenario"
|
||||
__name__ = 'mtm.scenario'
|
||||
|
||||
name = fields.Char("Scenario", required=True)
|
||||
valuation_date_type = fields.Selection(
|
||||
MTM_VALUATION_DATE_TYPES, "Valuation Date Type")
|
||||
valuation_date = fields.Date(
|
||||
"Valuation Date",
|
||||
states={
|
||||
'required': Eval('valuation_date_type') == 'fixed',
|
||||
},
|
||||
depends=['valuation_date_type'])
|
||||
resolved_valuation_date = fields.Function(
|
||||
fields.Date("Resolved Valuation Date"),
|
||||
'get_resolved_valuation_date')
|
||||
use_last_price = fields.Boolean("Use Last Available Price")
|
||||
calendar = fields.Many2One(
|
||||
'price.calendar', "Calendar"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def default_valuation_date_type():
|
||||
return 'fixed'
|
||||
|
||||
def get_resolved_valuation_date(self, name=None):
|
||||
return self.get_valuation_date()
|
||||
|
||||
def get_valuation_date(self):
|
||||
date_type = self.valuation_date_type or 'fixed'
|
||||
if date_type == 'fixed':
|
||||
return self.valuation_date
|
||||
|
||||
today = Pool().get('ir.date').today()
|
||||
if date_type == 'today':
|
||||
return today
|
||||
if date_type == 'month_start':
|
||||
return datetime.date(today.year, today.month, 1)
|
||||
if date_type == 'month_end':
|
||||
return datetime.date(
|
||||
today.year, today.month,
|
||||
calendar.monthrange(today.year, today.month)[1])
|
||||
if date_type == 'previous_month_end':
|
||||
return (
|
||||
datetime.date(today.year, today.month, 1)
|
||||
- datetime.timedelta(days=1))
|
||||
if date_type == 'next_month_end':
|
||||
year = today.year + (today.month // 12)
|
||||
month = (today.month % 12) + 1
|
||||
return datetime.date(
|
||||
year, month, calendar.monthrange(year, month)[1])
|
||||
if date_type == 'year_start':
|
||||
return datetime.date(today.year, 1, 1)
|
||||
if date_type == 'year_end':
|
||||
return datetime.date(today.year, 12, 31)
|
||||
return self.valuation_date
|
||||
|
||||
class MtmStrategy(ModelSQL, ModelView):
|
||||
"Mark to Market Strategy"
|
||||
__name__ = 'mtm.strategy'
|
||||
|
||||
@@ -598,16 +655,24 @@ class MtmStrategy(ModelSQL, ModelView):
|
||||
currency = default_itsa_currency()
|
||||
if currency:
|
||||
return currency
|
||||
|
||||
def get_mtm(self,line,qty):
|
||||
pool = Pool()
|
||||
Currency = pool.get('currency.currency')
|
||||
total = Decimal(0)
|
||||
|
||||
scenario = self.scenario
|
||||
dt = scenario.valuation_date
|
||||
|
||||
for comp in self.components:
|
||||
|
||||
@staticmethod
|
||||
def _scenario_valuation_date(scenario):
|
||||
get_valuation_date = getattr(
|
||||
type(scenario), 'get_valuation_date', None)
|
||||
if callable(get_valuation_date):
|
||||
return scenario.get_valuation_date()
|
||||
return scenario.valuation_date
|
||||
|
||||
def get_mtm(self,line,qty):
|
||||
pool = Pool()
|
||||
Currency = pool.get('currency.currency')
|
||||
total = Decimal(0)
|
||||
|
||||
scenario = self.scenario
|
||||
dt = self._scenario_valuation_date(scenario)
|
||||
|
||||
for comp in self.components:
|
||||
value = Decimal(0)
|
||||
|
||||
if comp.price_source_type == 'curve' and comp.price_index:
|
||||
@@ -662,13 +727,14 @@ class MtmStrategy(ModelSQL, ModelView):
|
||||
Snapshot = Pool().get('mtm.snapshot')
|
||||
|
||||
for strat in Strategy.search([('active', '=', True)]):
|
||||
amount = strat.compute_mtm()
|
||||
Snapshot.create([{
|
||||
'strategy': strat.id,
|
||||
'valuation_date': strat.scenario.valuation_date,
|
||||
'amount': amount,
|
||||
'currency': strat.currency.id,
|
||||
}])
|
||||
amount = strat.compute_mtm()
|
||||
valuation_date = strat._scenario_valuation_date(strat.scenario)
|
||||
Snapshot.create([{
|
||||
'strategy': strat.id,
|
||||
'valuation_date': valuation_date,
|
||||
'amount': amount,
|
||||
'currency': strat.currency.id,
|
||||
}])
|
||||
|
||||
class Mtm(ModelSQL, ModelView):
|
||||
"MtM Component"
|
||||
|
||||
@@ -20,6 +20,7 @@ from trytond.modules.purchase_trade import purchase as purchase_module
|
||||
from trytond.modules.purchase_trade import sale as sale_module
|
||||
from trytond.modules.purchase_trade import stock as stock_module
|
||||
from trytond.modules.purchase_trade import fee as fee_module
|
||||
from trytond.modules.purchase_trade import pricing as pricing_module
|
||||
from trytond.modules.purchase_trade import company_defaults
|
||||
from trytond.modules.purchase_trade.service import ContractFactory
|
||||
|
||||
@@ -715,6 +716,78 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
strategy.currency,
|
||||
relative_last=True)
|
||||
|
||||
def test_mtm_scenario_resolves_fixed_valuation_date(self):
|
||||
'mtm scenario can force an explicit valuation date'
|
||||
scenario = pricing_module.MtmScenario()
|
||||
scenario.valuation_date_type = 'fixed'
|
||||
scenario.valuation_date = datetime.date(2026, 6, 18)
|
||||
|
||||
self.assertEqual(
|
||||
scenario.get_valuation_date(),
|
||||
datetime.date(2026, 6, 18))
|
||||
|
||||
def test_mtm_scenario_resolves_relative_valuation_dates(self):
|
||||
'mtm scenario can resolve relative valuation dates'
|
||||
scenario = pricing_module.MtmScenario()
|
||||
Date = Mock()
|
||||
Date.today.return_value = datetime.date(2026, 2, 12)
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.pricing.Pool'
|
||||
) as PoolMock:
|
||||
PoolMock.return_value.get.return_value = Date
|
||||
|
||||
scenario.valuation_date_type = 'today'
|
||||
self.assertEqual(
|
||||
scenario.get_valuation_date(),
|
||||
datetime.date(2026, 2, 12))
|
||||
|
||||
scenario.valuation_date_type = 'month_end'
|
||||
self.assertEqual(
|
||||
scenario.get_valuation_date(),
|
||||
datetime.date(2026, 2, 28))
|
||||
|
||||
scenario.valuation_date_type = 'next_month_end'
|
||||
self.assertEqual(
|
||||
scenario.get_valuation_date(),
|
||||
datetime.date(2026, 3, 31))
|
||||
|
||||
@with_transaction()
|
||||
def test_get_mtm_uses_resolved_scenario_date(self):
|
||||
'get_mtm uses the scenario resolved valuation date'
|
||||
Strategy = Pool().get('mtm.strategy')
|
||||
strategy = Strategy()
|
||||
scenario = pricing_module.MtmScenario()
|
||||
scenario.valuation_date_type = 'month_end'
|
||||
scenario.use_last_price = True
|
||||
strategy.scenario = scenario
|
||||
strategy.currency = Mock()
|
||||
price_index = Mock(get_price=Mock(return_value=Decimal('100')))
|
||||
strategy.components = [Mock(
|
||||
price_source_type='curve',
|
||||
price_index=price_index,
|
||||
price_matrix=None,
|
||||
ratio=Decimal('100'),
|
||||
)]
|
||||
line = Mock(unit=Mock())
|
||||
Date = Mock()
|
||||
Date.today.return_value = datetime.date(2026, 2, 12)
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.pricing.Pool'
|
||||
) as PoolMock:
|
||||
PoolMock.return_value.get.return_value = Date
|
||||
|
||||
self.assertEqual(
|
||||
strategy.get_mtm(line, Decimal('2')),
|
||||
Decimal('200.00'))
|
||||
|
||||
price_index.get_price.assert_called_once_with(
|
||||
datetime.date(2026, 2, 28),
|
||||
line.unit,
|
||||
strategy.currency,
|
||||
relative_last=True)
|
||||
|
||||
@with_transaction()
|
||||
def test_get_mtm_uses_fixed_pricing_component(self):
|
||||
'get_mtm values fixed components like a constant price curve'
|
||||
|
||||
@@ -513,24 +513,25 @@ class ValuationBase(ModelSQL):
|
||||
scenario = getattr(strategy, 'scenario', None)
|
||||
if not scenario:
|
||||
return None
|
||||
valuation_date = cls._strategy_valuation_date(strategy)
|
||||
|
||||
for comp in strategy.components or []:
|
||||
value = Decimal(0)
|
||||
|
||||
if comp.price_source_type == 'curve' and comp.price_index:
|
||||
value = Decimal(comp.price_index.get_price(
|
||||
scenario.valuation_date,
|
||||
valuation_date,
|
||||
line.unit,
|
||||
strategy.currency,
|
||||
relative_last=scenario.use_last_price
|
||||
))
|
||||
elif comp.price_source_type == 'matrix' and comp.price_matrix:
|
||||
value = Decimal(strategy._get_matrix_price(
|
||||
comp, line, scenario.valuation_date))
|
||||
comp, line, valuation_date))
|
||||
|
||||
elif comp.price_source_type == 'fixed':
|
||||
value = Decimal(comp.get_price(
|
||||
scenario.valuation_date,
|
||||
valuation_date,
|
||||
line.unit,
|
||||
strategy.currency,
|
||||
relative_last=scenario.use_last_price
|
||||
@@ -543,6 +544,15 @@ class ValuationBase(ModelSQL):
|
||||
|
||||
return round(total, 4)
|
||||
|
||||
@staticmethod
|
||||
def _strategy_valuation_date(strategy):
|
||||
scenario = strategy.scenario
|
||||
get_valuation_date = getattr(
|
||||
type(scenario), 'get_valuation_date', None)
|
||||
if callable(get_valuation_date):
|
||||
return scenario.get_valuation_date()
|
||||
return scenario.valuation_date
|
||||
|
||||
@staticmethod
|
||||
def _supports_strategy_mtm(values):
|
||||
return values and values.get('type') in {'pur. priced', 'sale priced'}
|
||||
@@ -579,14 +589,15 @@ class ValuationBase(ModelSQL):
|
||||
@classmethod
|
||||
def _curve_component_price(cls, component, line, strategy):
|
||||
scenario = strategy.scenario
|
||||
valuation_date = cls._strategy_valuation_date(strategy)
|
||||
value = Decimal(component.price_index.get_price(
|
||||
scenario.valuation_date,
|
||||
valuation_date,
|
||||
line.unit,
|
||||
strategy.currency,
|
||||
relative_last=scenario.use_last_price))
|
||||
previous = cls._previous_curve_price(
|
||||
component.price_index,
|
||||
scenario.valuation_date,
|
||||
valuation_date,
|
||||
line.unit,
|
||||
strategy.currency)
|
||||
return round(value, 4), (
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<form>
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="valuation_date_type"/>
|
||||
<field name="valuation_date_type"/>
|
||||
<label name="valuation_date"/>
|
||||
<field name="valuation_date"/>
|
||||
<label name="resolved_valuation_date"/>
|
||||
<field name="resolved_valuation_date"/>
|
||||
<label name="use_last_price"/>
|
||||
<field name="use_last_price"/>
|
||||
<label name="calendar"/>
|
||||
<field name="calendar"/>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="valuation_date_type"/>
|
||||
<field name="valuation_date"/>
|
||||
<field name="resolved_valuation_date"/>
|
||||
<field name="use_last_price"/>
|
||||
<field name="calendar"/>
|
||||
</tree>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user