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. --> - + + +