Scenario Mtm

This commit is contained in:
2026-06-18 10:56:03 +02:00
parent a754e15e32
commit 248e410d88
5 changed files with 196 additions and 40 deletions

View File

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

View File

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

View File

@@ -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), (

View File

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

View File

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