diff --git a/.gitignore b/.gitignore
index faeab9b..09d1f25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*.pyc
+~$*
notes/accounting/excel_web_api/.env
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index aa43a4e..c2b9625 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -160,6 +160,62 @@ class PurchaseTradeTestCase(ModuleTestCase):
strategy, line),
Decimal('-349167.53'))
+ @with_transaction()
+ def test_strategy_mtm_lines_split_by_curve_with_previous_price(self):
+ 'strategy MTM creates one valuation line per curve with previous price'
+ Valuation = Pool().get('valuation.valuation')
+ valuation_date = datetime.date(2026, 6, 5)
+ previous_date = datetime.date(2026, 6, 4)
+ unit = Mock()
+ currency = Mock()
+ line = Mock(unit=unit)
+ curve_a = Mock(id=10)
+ curve_a.get_price.side_effect = [Decimal('100'), Decimal('90')]
+ curve_b = Mock(id=20)
+ curve_b.get_price.side_effect = [Decimal('50'), Decimal('45')]
+ strategy = Mock(
+ scenario=Mock(
+ valuation_date=valuation_date,
+ use_last_price=False),
+ currency=currency,
+ components=[
+ Mock(
+ price_source_type='curve',
+ price_index=curve_a,
+ ratio=Decimal('60')),
+ Mock(
+ price_source_type='curve',
+ price_index=curve_b,
+ ratio=Decimal('40')),
+ ])
+ values = {
+ 'type': 'pur. priced',
+ 'price': Decimal('10'),
+ 'amount': Decimal('-100'),
+ 'base_amount': Decimal('-100'),
+ 'quantity': Decimal('10'),
+ }
+ target = []
+ price_value = Mock()
+ price_value.search.return_value = [Mock(price_date=previous_date)]
+
+ with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
+ PoolMock.return_value.get.return_value = price_value
+ Valuation._append_strategy_mtm_lines(
+ target, values, strategy, line)
+
+ self.assertEqual(len(target), 2)
+ self.assertEqual(target[0]['mtm_curve'], curve_a.id)
+ self.assertEqual(target[0]['amount'], Decimal('-60.00'))
+ self.assertEqual(target[0]['mtm_price'], Decimal('60.0000'))
+ self.assertEqual(target[0]['mtm_price_prev'], Decimal('54.0000'))
+ self.assertEqual(target[0]['mtm'], Decimal('-600.00'))
+ self.assertEqual(target[1]['mtm_curve'], curve_b.id)
+ self.assertEqual(target[1]['amount'], Decimal('-40.00'))
+ self.assertEqual(target[1]['mtm_price'], Decimal('20.0000'))
+ self.assertEqual(target[1]['mtm_price_prev'], Decimal('18.0000'))
+ self.assertEqual(target[1]['mtm'], Decimal('-200.00'))
+
@with_transaction()
def test_purchase_line_charter_conditions_inherit_header_when_empty(self):
'purchase line uses header charter conditions when it has no line terms'
diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py
index f770d44..cb41ab8 100644
--- a/modules/purchase_trade/valuation.py
+++ b/modules/purchase_trade/valuation.py
@@ -57,6 +57,8 @@ class ValuationBase(ModelSQL):
unit = fields.Many2One('product.uom',"Unit")
amount = fields.Numeric("Amount",digits=(16,2))
mtm_price = fields.Numeric("Mtm Price", digits=(16,4))
+ mtm_price_prev = fields.Numeric("Mtm Price -1", digits=(16,4))
+ mtm_curve = fields.Many2One('price.price', "Curve")
mtm = fields.Numeric("Mtm",digits=(16,2))
strategy = fields.Many2One('mtm.strategy',"Strategy")
lot = fields.Many2One('lot.lot',"Lot")
@@ -387,6 +389,122 @@ class ValuationBase(ModelSQL):
return -abs(mtm)
return abs(mtm)
+ @staticmethod
+ def _component_weight(component):
+ ratio = Decimal(component.ratio or 0)
+ return abs(ratio) if ratio else Decimal(100)
+
+ @classmethod
+ def _previous_curve_price(cls, curve, price_date, unit, currency):
+ PriceValue = Pool().get('price.price_value')
+ previous = PriceValue.search([
+ ('price', '=', curve.id),
+ ('price_date', '<', price_date),
+ ], order=[('price_date', 'DESC')], limit=1)
+ if not previous:
+ return None
+ return Decimal(curve.get_price(
+ previous[0].price_date, unit, currency, last=False))
+
+ @classmethod
+ def _curve_component_price(cls, component, line, strategy):
+ scenario = strategy.scenario
+ value = Decimal(component.price_index.get_price(
+ scenario.valuation_date,
+ line.unit,
+ strategy.currency,
+ last=scenario.use_last_price))
+ previous = cls._previous_curve_price(
+ component.price_index,
+ scenario.valuation_date,
+ line.unit,
+ strategy.currency)
+ if component.ratio:
+ ratio = Decimal(component.ratio) / Decimal(100)
+ value *= ratio
+ if previous is not None:
+ previous *= ratio
+ return round(value, 4), (
+ round(previous, 4) if previous is not None else None)
+
+ @classmethod
+ def _split_value(cls, value, share, digits=2):
+ if value in (None, ''):
+ return value
+ return round(Decimal(value) * share, digits)
+
+ @classmethod
+ def _append_strategy_mtm_lines(cls, target, values, strategy, line):
+ components = list(strategy.components or [])
+ curve_components = [
+ component for component in components
+ if component.price_source_type == 'curve'
+ and component.price_index]
+ if not curve_components:
+ line_values = dict(values)
+ line_values['mtm_price'] = cls._get_strategy_mtm_price(
+ strategy, line)
+ line_values['mtm'] = cls._signed_strategy_mtm(
+ values, strategy, line)
+ line_values['strategy'] = strategy
+ target.append(line_values)
+ return
+
+ curve_component_ids = {id(component) for component in curve_components}
+ total_weight = sum(
+ cls._component_weight(component)
+ for component in components) or Decimal(100)
+ total_mtm_price = cls._get_strategy_mtm_price(strategy, line)
+ curve_mtm_price = Decimal(0)
+ signed_factor = Decimal(1)
+ amount = values.get('amount')
+ if amount and amount < 0:
+ signed_factor = Decimal(-1)
+ elif not amount and values.get('type') in {'pur. priced', 'pur. efp'}:
+ signed_factor = Decimal(-1)
+
+ for component in curve_components:
+ share = cls._component_weight(component) / total_weight
+ mtm_price, mtm_price_prev = cls._curve_component_price(
+ component, line, strategy)
+ curve_mtm_price += mtm_price
+ line_values = dict(values)
+ line_values['price'] = cls._split_value(
+ values.get('price'), share, digits=4)
+ line_values['amount'] = cls._split_value(
+ values.get('amount'), share)
+ line_values['base_amount'] = cls._split_value(
+ values.get('base_amount'), share)
+ line_values['mtm_price'] = mtm_price
+ line_values['mtm_price_prev'] = mtm_price_prev
+ line_values['mtm_curve'] = component.price_index.id
+ line_values['mtm'] = round(
+ mtm_price * Decimal(values['quantity']) * signed_factor, 2)
+ line_values['strategy'] = strategy
+ target.append(line_values)
+
+ residual_weight = sum(
+ cls._component_weight(component)
+ for component in components
+ if id(component) not in curve_component_ids)
+ if residual_weight:
+ share = residual_weight / total_weight
+ mtm_price = round(total_mtm_price - curve_mtm_price, 4)
+ line_values = dict(values)
+ line_values['price'] = cls._split_value(
+ values.get('price'), share, digits=4)
+ line_values['amount'] = cls._split_value(
+ values.get('amount'), share)
+ line_values['base_amount'] = cls._split_value(
+ values.get('base_amount'), share)
+ line_values['mtm_price'] = mtm_price
+ line_values['mtm_price_prev'] = None
+ line_values['mtm_curve'] = None
+ line_values['mtm'] = round(
+ mtm_price * Decimal(values['quantity']) * signed_factor, 2)
+ line_values['strategy'] = strategy
+ target.append(line_values)
+
@staticmethod
def _get_basis_component_total(record):
getter = getattr(record, '_get_basis_component_price', None)
@@ -537,12 +655,8 @@ class ValuationBase(ModelSQL):
cls._set_matched_sale_values(values, matched_sale_line)
if line.mtm and cls._supports_strategy_mtm(values):
for strat in line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, line)
else:
if values:
price_lines.append(values)
@@ -555,12 +669,8 @@ class ValuationBase(ModelSQL):
cls._set_matched_sale_values(values, matched_sale_line)
if line.mtm and cls._supports_strategy_mtm(values):
for strat in line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, line)
else:
if values:
price_lines.append(values)
@@ -578,12 +688,8 @@ class ValuationBase(ModelSQL):
cls._set_matched_sale_values(values, matched_sale_line)
if line.mtm and cls._supports_strategy_mtm(values):
for strat in line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, line)
else:
if values:
price_lines.append(values)
@@ -613,12 +719,8 @@ class ValuationBase(ModelSQL):
extra_price=premium_delta)
if sl_line.mtm and cls._supports_strategy_mtm(values):
for strat in line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, sl_line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, sl_line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, sl_line)
else:
if values:
price_lines.append(values)
@@ -635,12 +737,8 @@ class ValuationBase(ModelSQL):
)
if sl_line.mtm and cls._supports_strategy_mtm(values):
for strat in sl_line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, sl_line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, sl_line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, sl_line)
else:
if values:
price_lines.append(values)
@@ -761,12 +859,8 @@ class ValuationBase(ModelSQL):
)
if sale_line.mtm and cls._supports_strategy_mtm(values):
for strat in sale_line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, sale_line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, sale_line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, sale_line)
else:
if values:
price_lines.append(values)
@@ -778,12 +872,8 @@ class ValuationBase(ModelSQL):
extra_price=premium_delta)
if sale_line.mtm and cls._supports_strategy_mtm(values):
for strat in sale_line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, sale_line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, sale_line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, sale_line)
else:
if values:
price_lines.append(values)
@@ -801,12 +891,8 @@ class ValuationBase(ModelSQL):
)
if sale_line.mtm and cls._supports_strategy_mtm(values):
for strat in sale_line.mtm:
- values['mtm_price'] = cls._get_strategy_mtm_price(strat, sale_line)
- values['mtm'] = cls._signed_strategy_mtm(values, strat, sale_line)
- values['strategy'] = strat
-
- if values:
- price_lines.append(values)
+ cls._append_strategy_mtm_lines(
+ price_lines, values, strat, sale_line)
else:
if values:
price_lines.append(values)
diff --git a/modules/purchase_trade/view/valuation_list.xml b/modules/purchase_trade/view/valuation_list.xml
index 8356066..5d6f03b 100644
--- a/modules/purchase_trade/view/valuation_list.xml
+++ b/modules/purchase_trade/view/valuation_list.xml
@@ -14,6 +14,12 @@
-
+
+
+
diff --git a/modules/purchase_trade/view/valuation_tree_sequence3.xml b/modules/purchase_trade/view/valuation_tree_sequence3.xml
index 95c8f1b..30dfa21 100755
--- a/modules/purchase_trade/view/valuation_tree_sequence3.xml
+++ b/modules/purchase_trade/view/valuation_tree_sequence3.xml
@@ -13,6 +13,12 @@ this repository contains the full copyright notices and license terms. -->
-
+
+
+