From 6ac19a1334506e5c57baa2dbeec459e1c4f49f6c Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 11 Jun 2026 14:31:35 +0200 Subject: [PATCH] Pricing default --- modules/purchase_trade/pricing.py | 112 +++++++++++++++++--- modules/purchase_trade/tests/test_module.py | 87 +++++++++++++++ 2 files changed, 187 insertions(+), 12 deletions(-) diff --git a/modules/purchase_trade/pricing.py b/modules/purchase_trade/pricing.py index df9a8a2..568ecf5 100755 --- a/modules/purchase_trade/pricing.py +++ b/modules/purchase_trade/pricing.py @@ -863,18 +863,106 @@ class Component(ModelSQL, ModelView): def set_cur(cls, components, name, value): cls.write(components, {'fixed_currency': value}) - def get_calendar(self): - if self.calendar: - return self.calendar - if self.price_matrix: - return self.price_matrix.calendar - - @staticmethod - def _record_id(record): - return getattr(record, 'id', record) - - @classmethod - def _matches_optional_field(cls, matrix_line, field_name, target): + def get_calendar(self): + if self.calendar: + return self.calendar + if self.price_matrix: + return self.price_matrix.calendar + + @staticmethod + def _record_id(record): + return getattr(record, 'id', record) + + @classmethod + def _trigger_values(cls, trigger): + return { + 'pricing_period': cls._record_id( + getattr(trigger, 'pricing_period', None)), + 'from_p': getattr(trigger, 'from_p', None), + 'to_p': getattr(trigger, 'to_p', None), + 'average': getattr(trigger, 'average', None), + 'last': getattr(trigger, 'last', None), + 'application_period': cls._record_id( + getattr(trigger, 'application_period', None)), + 'from_a': getattr(trigger, 'from_a', None), + 'to_a': getattr(trigger, 'to_a', None), + } + + @classmethod + def _copy_trigger_values(cls, component): + return [ + cls._trigger_values(trigger) + for trigger in getattr(component, 'triggers', [])] + + @classmethod + def _first_component_domain(cls, line=None, sale_line=None, exclude=None): + domain = [] + if line: + domain.append(('line', '=', cls._record_id(line))) + elif sale_line: + domain.append(('sale_line', '=', cls._record_id(sale_line))) + else: + return None + if exclude: + domain.append(('id', '!=', cls._record_id(exclude))) + return domain + + @classmethod + def _get_first_component(cls, line=None, sale_line=None, exclude=None): + domain = cls._first_component_domain(line, sale_line, exclude) + if not domain: + return None + components = cls.search(domain, order=[('id', 'ASC')], limit=1) + return components[0] if components else None + + @classmethod + def _default_trigger_values(cls, line=None, sale_line=None, exclude=None): + component = cls._get_first_component(line, sale_line, exclude) + if not component: + return [] + return cls._copy_trigger_values(component) + + @classmethod + def default_triggers(cls): + context = Transaction().context + return cls._default_trigger_values( + line=context.get('default_line'), + sale_line=context.get('default_sale_line')) + + def _inherit_first_component_triggers(self): + if getattr(self, 'triggers', None): + return + values = self._default_trigger_values( + line=getattr(self, 'line', None), + sale_line=getattr(self, 'sale_line', None), + exclude=getattr(self, 'id', None)) + if values: + Trigger = Pool().get('pricing.trigger') + self.triggers = [Trigger(**value) for value in values] + + @fields.depends('line', 'sale_line', 'triggers') + def on_change_line(self): + self._inherit_first_component_triggers() + + @fields.depends('line', 'sale_line', 'triggers') + def on_change_sale_line(self): + self._inherit_first_component_triggers() + + @classmethod + def create(cls, vlist): + vlist = [values.copy() for values in vlist] + for values in vlist: + if values.get('triggers'): + continue + trigger_values = cls._default_trigger_values( + line=values.get('line'), + sale_line=values.get('sale_line')) + if trigger_values: + values['triggers'] = [('create', trigger_values)] + return super(Component, cls).create(vlist) + + @classmethod + def _matches_optional_field(cls, matrix_line, field_name, target): value = getattr(matrix_line, field_name, None) if not value: return True, 0 diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index de29f35..6c8f777 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2653,6 +2653,93 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertIsNone(pricing.price_component) + def test_pricing_component_default_triggers_copy_first_purchase_component(self): + 'new purchase components inherit trigger values from the first component' + Component = Pool().get('pricing.component') + pricing_period = Mock(id=21) + application_period = Mock(id=22) + first_component = Mock(triggers=[ + Mock( + pricing_period=pricing_period, + from_p=datetime.date(2026, 4, 1), + to_p=datetime.date(2026, 4, 5), + average=True, + last=False, + application_period=application_period, + from_a=datetime.date(2026, 5, 1), + to_a=datetime.date(2026, 5, 5), + ), + ]) + + with patch.object(Component, 'search', + return_value=[first_component]) as search: + values = Component._default_trigger_values(line=10) + + search.assert_called_once_with( + [('line', '=', 10)], order=[('id', 'ASC')], limit=1) + self.assertEqual(values, [{ + 'pricing_period': 21, + 'from_p': datetime.date(2026, 4, 1), + 'to_p': datetime.date(2026, 4, 5), + 'average': True, + 'last': False, + 'application_period': 22, + 'from_a': datetime.date(2026, 5, 1), + 'to_a': datetime.date(2026, 5, 5), + }]) + + def test_pricing_component_create_inherits_first_sale_component_triggers(self): + 'component create copies triggers from the first sale line component' + Component = Pool().get('pricing.component') + first_component = Mock(triggers=[ + Mock( + pricing_period=Mock(id=21), + from_p=datetime.date(2026, 4, 1), + to_p=datetime.date(2026, 4, 5), + average=True, + last=True, + application_period=None, + from_a=None, + to_a=None, + ), + ]) + + with patch.object(Component, 'search', + return_value=[first_component]) as search, patch( + 'trytond.modules.purchase_trade.pricing.super') as super_mock: + Component.create([{'sale_line': 10, 'price_source_type': 'curve'}]) + + search.assert_called_once_with( + [('sale_line', '=', 10)], order=[('id', 'ASC')], limit=1) + created_values = super_mock.return_value.create.call_args.args[0][0] + self.assertEqual(created_values['triggers'], [('create', [{ + 'pricing_period': 21, + 'from_p': datetime.date(2026, 4, 1), + 'to_p': datetime.date(2026, 4, 5), + 'average': True, + 'last': True, + 'application_period': None, + 'from_a': None, + 'to_a': None, + }])]) + + def test_pricing_component_create_keeps_explicit_triggers(self): + 'component create does not replace triggers already provided' + Component = Pool().get('pricing.component') + explicit_triggers = [('create', [{'average': False}])] + + with patch.object(Component, 'search') as search, patch( + 'trytond.modules.purchase_trade.pricing.super') as super_mock: + Component.create([{ + 'line': 10, + 'price_source_type': 'curve', + 'triggers': explicit_triggers, + }]) + + search.assert_not_called() + created_values = super_mock.return_value.create.call_args.args[0][0] + self.assertIs(created_values['triggers'], explicit_triggers) + def test_pricing_component_must_belong_to_pricing_owner(self): 'pricing rows reject components from another purchase or sale line' Pricing = Pool().get('pricing.pricing')