From f45cdb08079cc068080e73e9fcd547b15a0e3ed0 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sun, 28 Jun 2026 13:31:48 +0200 Subject: [PATCH] ITSA points 28.06 --- modules/purchase/purchase.xml | Bin 34119 -> 27540 bytes modules/purchase_trade/__init__.py | 6 + modules/purchase_trade/company_defaults.py | 17 ++ modules/purchase_trade/configuration.py | 22 ++ modules/purchase_trade/configuration.xml | 10 + modules/purchase_trade/ctrm_reporting.py | 4 +- modules/purchase_trade/pricing.py | 83 +++++-- modules/purchase_trade/purchase.py | 131 ++++++++++- modules/purchase_trade/sale.py | 122 ++++++++++ modules/purchase_trade/tests/test_module.py | 221 +++++++++++++++++- modules/purchase_trade/valuation.py | 108 ++++++++- .../view/purchase_configuration_form.xml | 8 + modules/purchase_trade/view/purchase_form.xml | 3 + .../view/sale_configuration_form.xml | 8 + modules/purchase_trade/view/sale_form.xml | 3 + .../purchase_trade/view/valuation_list.xml | 3 +- .../view/valuation_tree_sequence3.xml | 3 +- .../view/valuation_tree_sequence4.xml | 1 + 18 files changed, 718 insertions(+), 35 deletions(-) create mode 100644 modules/purchase_trade/view/purchase_configuration_form.xml create mode 100644 modules/purchase_trade/view/sale_configuration_form.xml diff --git a/modules/purchase/purchase.xml b/modules/purchase/purchase.xml index d83cd92b6fc4e8d2eec5738c06f7846bd76800a0..cc749f2b0a3044507027fec01c9cb666ba5c9995 100755 GIT binary patch literal 27540 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 diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index d46906b..9f0e81b 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -96,6 +96,12 @@ def register(): pricing.ImportPricesResult, duplicate.TradeCustomDuplicateStart, module='purchase_trade', type_='model') + Pool.register( + configuration.PurchaseConfiguration, + module='purchase', type_='model') + Pool.register( + configuration.SaleConfiguration, + module='sale', type_='model') Pool.register( incoming.ImportSwift, incoming.PrepareDocuments, diff --git a/modules/purchase_trade/company_defaults.py b/modules/purchase_trade/company_defaults.py index 3d8f6d5..5a3283d 100644 --- a/modules/purchase_trade/company_defaults.py +++ b/modules/purchase_trade/company_defaults.py @@ -41,6 +41,23 @@ def default_itsa_currency(): return record_id(currency) if currency else None +def default_itsa_price_fix_type(): + fix_type = _search_first('price.fixtype', [ + [('name', '=', 'Market Price')], + [('name', '=', 'Market price')], + [('name', 'ilike', 'Market Price')], + ]) + return record_id(fix_type) if fix_type else None + + +def default_itsa_price_calendar(): + calendar = _search_first('price.calendar', [ + [('name', '=', 'Argus Sulphuric Acid')], + [('name', 'ilike', 'Argus Sulphuric Acid')], + ]) + return record_id(calendar) if calendar else None + + def default_itsa_unit(): unit = _search_first('product.uom', [ [('symbol', '=', 'Mt')], diff --git a/modules/purchase_trade/configuration.py b/modules/purchase_trade/configuration.py index 6a574d4..f664668 100644 --- a/modules/purchase_trade/configuration.py +++ b/modules/purchase_trade/configuration.py @@ -4,6 +4,28 @@ from trytond.pyson import Eval from trytond.transaction import Transaction +class PurchaseConfiguration(metaclass=PoolMeta): + __name__ = 'purchase.configuration' + + allow_modification_after_validation = fields.Boolean( + "Autorise modification after validation") + + @classmethod + def default_allow_modification_after_validation(cls): + return False + + +class SaleConfiguration(metaclass=PoolMeta): + __name__ = 'sale.configuration' + + allow_modification_after_validation = fields.Boolean( + "Autorise modification after validation") + + @classmethod + def default_allow_modification_after_validation(cls): + return False + + class AccountConfiguration(metaclass=PoolMeta): __name__ = 'account.configuration' diff --git a/modules/purchase_trade/configuration.xml b/modules/purchase_trade/configuration.xml index edcccc9..8946caa 100644 --- a/modules/purchase_trade/configuration.xml +++ b/modules/purchase_trade/configuration.xml @@ -10,6 +10,16 @@ form template_configuration_form + + purchase.configuration + + purchase_configuration_form + + + sale.configuration + + sale_configuration_form + Pricing Configuration diff --git a/modules/purchase_trade/ctrm_reporting.py b/modules/purchase_trade/ctrm_reporting.py index 95e8518..1865bec 100644 --- a/modules/purchase_trade/ctrm_reporting.py +++ b/modules/purchase_trade/ctrm_reporting.py @@ -21,7 +21,7 @@ PHYSICAL_VALUATION_TYPES = [ 'market', ] DERIVATIVE_VALUATION_TYPES = ['derivative'] -MTM_VALUATION_TYPES = ['mtm'] +MTM_VALUATION_TYPES = ['mtm', 'pur. mtm', 'sale mtm'] ALL_VALUATION_TYPES = ( PHYSICAL_VALUATION_TYPES + DERIVATIVE_VALUATION_TYPES + MTM_VALUATION_TYPES) @@ -496,6 +496,8 @@ class CTRMMtmPnl(ModelSQL, ModelView): ('shipment fee', 'Shipment fee'), ('market', 'Market'), ('mtm', 'Mtm'), + ('pur. mtm', 'Pur. Mtm'), + ('sale mtm', 'Sale Mtm'), ('derivative', 'Derivative'), ], "Type") reference = fields.Char("Reference") diff --git a/modules/purchase_trade/pricing.py b/modules/purchase_trade/pricing.py index 486a972..77c8b3b 100755 --- a/modules/purchase_trade/pricing.py +++ b/modules/purchase_trade/pricing.py @@ -25,7 +25,8 @@ from io import BytesIO from xml.etree import ElementTree from trytond.modules.purchase_trade.purchase import (TRIGGERS) from trytond.modules.purchase_trade.company_defaults import ( - default_itsa_currency, default_itsa_unit, is_itsa_company) + default_itsa_currency, default_itsa_price_calendar, + default_itsa_price_fix_type, default_itsa_unit, is_itsa_company) logger = logging.getLogger(__name__) @@ -594,6 +595,13 @@ class MtmScenario(ModelSQL, ModelView): def default_valuation_date_type(): return 'fixed' + @classmethod + def default_calendar(cls): + if is_itsa_company(): + calendar = default_itsa_price_calendar() + if calendar: + return calendar + def get_resolved_valuation_date(self, name=None): return self.get_valuation_date() @@ -777,12 +785,14 @@ class Mtm(ModelSQL, ModelView): currency = fields.Many2One('currency.currency', "Currency") - def get_cur(self, name=None): - if self.price_index: - return self.price_index.price_currency - if self.price_matrix: - return self.price_matrix.currency - return None + def get_cur(self, name=None): + if self.price_index: + return self.price_index.price_currency + if self.price_matrix: + return self.price_matrix.currency + if self.currency: + return self.currency + return None @fields.depends('price_index','price_matrix') def on_change_with_currency(self): @@ -795,6 +805,18 @@ class Mtm(ModelSQL, ModelView): if currency: return currency + @classmethod + def default_fix_type(cls): + if is_itsa_company(): + fix_type = default_itsa_price_fix_type() + if fix_type: + return fix_type + + @classmethod + def default_ratio(cls): + if is_itsa_company(): + return Decimal('100') + class PriceMatrix(ModelSQL, ModelView): "Price Matrix" __name__ = 'price.matrix' @@ -934,26 +956,24 @@ class Component(ModelSQL, ModelView): else: return '[' + self.fix_type.name + '] ' - def get_cur(self, name=None): - if self.price_index: - PI = Pool().get('price.price') - pi = PI(self.price_index) - return pi.price_currency - if self.price_matrix: - return self.price_matrix.currency - if self.price_source_type == 'fixed': - return self.fixed_currency - - @fields.depends( - 'price_source_type', 'price_index', 'price_matrix', - 'fixed_currency') + def get_cur(self, name=None): + if self.price_index: + PI = Pool().get('price.price') + pi = PI(self.price_index) + return pi.price_currency + if self.price_matrix: + return self.price_matrix.currency + return self.fixed_currency + + @fields.depends( + 'price_source_type', 'price_index', 'price_matrix', + 'fixed_currency') def on_change_with_currency(self): if self.price_source_type == 'curve' and self.price_index: return self.get_cur() if self.price_source_type == 'matrix' and self.price_matrix: return self.get_cur() - if self.price_source_type == 'fixed': - return self.fixed_currency + return self.fixed_currency @classmethod def default_fixed_currency(cls): @@ -961,6 +981,25 @@ class Component(ModelSQL, ModelView): currency = default_itsa_currency() if currency: return currency + + @classmethod + def default_fix_type(cls): + if is_itsa_company(): + fix_type = default_itsa_price_fix_type() + if fix_type: + return fix_type + + @classmethod + def default_ratio(cls): + if is_itsa_company(): + return Decimal('100') + + @classmethod + def default_calendar(cls): + if is_itsa_company(): + calendar = default_itsa_price_calendar() + if calendar: + return calendar @classmethod def set_cur(cls, components, name, value): diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 3a130a2..f9d05da 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -272,6 +272,10 @@ class Purchase(metaclass=PoolMeta): def __setup__(cls): super().__setup__() cls._transitions.discard(('confirmed', 'processing')) + cls.lines.states['readonly'] = ( + (Eval('state') != 'draft') + & ((Eval('state') != 'quotation') + | ~Eval('allow_modification_after_validation'))) cls._buttons['process'] = { 'invisible': True, 'depends': ['state'], @@ -380,11 +384,14 @@ class Purchase(metaclass=PoolMeta): doc_template = fields.Many2One('doc.template',"Template") required_documents = fields.Many2Many( 'contract.document.type', 'purchase', 'doc_type', 'Required Documents') - analytic_dimensions = fields.One2Many( - 'analytic.dimension.assignment', - 'purchase', - 'Analytic Dimensions' - ) + analytic_dimensions = fields.One2Many( + 'analytic.dimension.assignment', + 'purchase', + 'Analytic Dimensions' + ) + allow_modification_after_validation = fields.Function( + fields.Boolean("Autorise modification after validation"), + 'on_change_with_allow_modification_after_validation') trader = fields.Many2One( 'party.party', "Trader", domain=[('categories.name', '=', 'TRADER')]) @@ -404,6 +411,13 @@ class Purchase(metaclass=PoolMeta): self.company and self.company.party and self.company.party.name in {'MELYA', 'ITSA'}) + def on_change_with_allow_modification_after_validation(self, name=None): + Configuration = Pool().get('purchase.configuration') + configurations = Configuration.search([], limit=1) + return bool( + configurations + and configurations[0].allow_modification_after_validation) + def _get_default_bank_account(self): if not self.party or not self.party.bank_accounts: return None @@ -2087,15 +2101,97 @@ class Line(metaclass=PoolMeta): raise UserError( "Shipment period From date must be before To date.") + @classmethod + def _estimated_bl_relation_field(cls): + return 'line' + + @classmethod + def _has_estimated_bl_date(cls, estimated_dates): + return any( + getattr(estimated, 'trigger', None) == 'bldate' + for estimated in (estimated_dates or [])) + + @classmethod + def _values_have_estimated_bl_date(cls, values): + for command in values.get('estimated_date') or []: + action = command[0] + if action == 'create': + if any( + estimated.get('trigger') == 'bldate' + for estimated in command[1]): + return True + elif action == 'write': + actions = iter(command[1:]) + for _estimated_ids, estimated_values in zip(actions, actions): + if estimated_values.get('trigger') == 'bldate': + return True + return False + + @classmethod + def _delivery_period_from_values(cls, values): + period = values.get('del_period') + if period and not hasattr(period, 'beg_date'): + period = Pool().get('product.month')(period) + return period + + @classmethod + def _default_estimated_bl_date(cls, values): + period = cls._delivery_period_from_values(values) + return getattr(period, 'beg_date', None) + + @classmethod + def _set_default_estimated_bl_date_values(cls, values): + if cls._values_have_estimated_bl_date(values): + return + estimated_date = cls._default_estimated_bl_date(values) + if not estimated_date: + return + if values.get('estimated_date') is None: + values['estimated_date'] = [] + values['estimated_date'].append(('create', [{ + 'trigger': 'bldate', + 'estimated_date': estimated_date, + }])) + + @classmethod + def _create_missing_estimated_bl_dates(cls, lines): + Estimated = Pool().get('pricing.estimated') + values = [] + relation_field = cls._estimated_bl_relation_field() + for line in lines: + if cls._has_estimated_bl_date(getattr(line, 'estimated_date', None)): + continue + del_period = getattr(line, 'del_period', None) + estimated_date = getattr(del_period, 'beg_date', None) + if not estimated_date: + continue + values.append({ + relation_field: line.id, + 'trigger': 'bldate', + 'estimated_date': estimated_date, + }) + if values: + Estimated.create(values) + @classmethod def create(cls, vlist): + regenerate_valuation = any( + cls._should_regenerate_valuation(values) for values in vlist) for values in vlist: cls._check_delivery_period_values([cls()], values) + cls._set_default_estimated_bl_date_values(values) cls._set_initial_quantity_values(values) lines = super().create(vlist) if not Transaction().context.get('_purchase_trade_skip_fee_rules'): Pool().get('fee.rule').apply_to_lines( lines, 'purchase_line', auto_only=True) + if (regenerate_valuation + and not Transaction().context.get( + '_purchase_trade_skip_valuation_regeneration')): + Valuation = Pool().get('valuation.valuation') + with Transaction().set_context( + _purchase_trade_skip_valuation_regeneration=True): + Valuation.regenerate_for_purchase_lines(lines) return lines @classmethod @@ -2689,12 +2785,28 @@ class Line(metaclass=PoolMeta): def on_change_linked_unit(self): self._recompute_trade_price_fields() + @classmethod + def _valuation_regeneration_fields(cls): + return { + 'quantity', 'quantity_theorical', 'unit', 'unit_price', + 'price_type', 'premium', 'linked_price', 'linked_currency', + 'linked_unit', 'price_pricing', 'price_components', 'derivatives', + 'fees', 'lots', 'product', 'currency', + } + + @classmethod + def _should_regenerate_valuation(cls, values): + return bool(cls._valuation_regeneration_fields() & set(values)) + @classmethod def write(cls, *args): actions = iter(args) args = [] + valuation_line_ids = set() for records, values in zip(actions, actions): cls._check_delivery_period_values(records, values) + if cls._should_regenerate_valuation(values): + valuation_line_ids.update(record.id for record in records) args.extend((records, values)) # Agents: @@ -2748,9 +2860,18 @@ class Line(metaclass=PoolMeta): Pool().get('lot.lot').assert_lines_quantity_consistency( cls._fresh_lines_for_quantity_consistency(lines)) + cls._create_missing_estimated_bl_dates(lines) if not Transaction().context.get('_purchase_trade_skip_fee_rules'): Pool().get('fee.rule').apply_to_lines( lines, 'purchase_line', auto_only=True) + if (valuation_line_ids + and not Transaction().context.get( + '_purchase_trade_skip_valuation_regeneration')): + Valuation = Pool().get('valuation.valuation') + with Transaction().set_context( + _purchase_trade_skip_valuation_regeneration=True): + Valuation.regenerate_for_purchase_lines( + cls.browse(list(valuation_line_ids))) @classmethod def _sync_open_lot_quantity(cls, line, vlot, target_quantity): diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index ae3e2b8..d0c7c60 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -237,6 +237,10 @@ class Sale(metaclass=PoolMeta): def __setup__(cls): super().__setup__() cls._transitions.discard(('confirmed', 'processing')) + cls.lines.states['readonly'] = ( + (Eval('state') != 'draft') + & ((Eval('state') != 'quotation') + | ~Eval('allow_modification_after_validation'))) cls._buttons['process'] = { 'invisible': True, 'depends': ['state'], @@ -329,6 +333,9 @@ class Sale(metaclass=PoolMeta): 'sale', 'Analytic Dimensions' ) + allow_modification_after_validation = fields.Function( + fields.Boolean("Autorise modification after validation"), + 'on_change_with_allow_modification_after_validation') trader = fields.Many2One( 'party.party', "Trader", domain=[('categories.name', '=', 'TRADER')]) @@ -348,6 +355,13 @@ class Sale(metaclass=PoolMeta): self.company and self.company.party and self.company.party.name in {'MELYA', 'ITSA'}) + def on_change_with_allow_modification_after_validation(self, name=None): + Configuration = Pool().get('sale.configuration') + configurations = Configuration.search([], limit=1) + return bool( + configurations + and configurations[0].allow_modification_after_validation) + def _get_default_bank_account(self): if not self.party or not self.party.bank_accounts: return None @@ -1911,15 +1925,97 @@ class SaleLine(metaclass=PoolMeta): raise UserError( "Shipment period From date must be before To date.") + @classmethod + def _estimated_bl_relation_field(cls): + return 'sale_line' + + @classmethod + def _has_estimated_bl_date(cls, estimated_dates): + return any( + getattr(estimated, 'trigger', None) == 'bldate' + for estimated in (estimated_dates or [])) + + @classmethod + def _values_have_estimated_bl_date(cls, values): + for command in values.get('estimated_date') or []: + action = command[0] + if action == 'create': + if any( + estimated.get('trigger') == 'bldate' + for estimated in command[1]): + return True + elif action == 'write': + actions = iter(command[1:]) + for _estimated_ids, estimated_values in zip(actions, actions): + if estimated_values.get('trigger') == 'bldate': + return True + return False + + @classmethod + def _delivery_period_from_values(cls, values): + period = values.get('del_period') + if period and not hasattr(period, 'beg_date'): + period = Pool().get('product.month')(period) + return period + + @classmethod + def _default_estimated_bl_date(cls, values): + period = cls._delivery_period_from_values(values) + return getattr(period, 'beg_date', None) + + @classmethod + def _set_default_estimated_bl_date_values(cls, values): + if cls._values_have_estimated_bl_date(values): + return + estimated_date = cls._default_estimated_bl_date(values) + if not estimated_date: + return + if values.get('estimated_date') is None: + values['estimated_date'] = [] + values['estimated_date'].append(('create', [{ + 'trigger': 'bldate', + 'estimated_date': estimated_date, + }])) + + @classmethod + def _create_missing_estimated_bl_dates(cls, lines): + Estimated = Pool().get('pricing.estimated') + values = [] + relation_field = cls._estimated_bl_relation_field() + for line in lines: + if cls._has_estimated_bl_date(getattr(line, 'estimated_date', None)): + continue + del_period = getattr(line, 'del_period', None) + estimated_date = getattr(del_period, 'beg_date', None) + if not estimated_date: + continue + values.append({ + relation_field: line.id, + 'trigger': 'bldate', + 'estimated_date': estimated_date, + }) + if values: + Estimated.create(values) + @classmethod def create(cls, vlist): + regenerate_valuation = any( + cls._should_regenerate_valuation(values) for values in vlist) for values in vlist: cls._check_delivery_period_values([cls()], values) + cls._set_default_estimated_bl_date_values(values) cls._set_initial_quantity_values(values) lines = super().create(vlist) if not Transaction().context.get('_purchase_trade_skip_fee_rules'): Pool().get('fee.rule').apply_to_lines( lines, 'sale_line', auto_only=True) + if (regenerate_valuation + and not Transaction().context.get( + '_purchase_trade_skip_valuation_regeneration')): + Valuation = Pool().get('valuation.valuation') + with Transaction().set_context( + _purchase_trade_skip_valuation_regeneration=True): + Valuation.regenerate_for_sale_lines(lines) return lines @classmethod @@ -2631,12 +2727,28 @@ class SaleLine(metaclass=PoolMeta): Pricing.save([p]) index += 1 + @classmethod + def _valuation_regeneration_fields(cls): + return { + 'quantity', 'quantity_theorical', 'unit', 'unit_price', + 'price_type', 'premium', 'linked_price', 'linked_currency', + 'linked_unit', 'price_pricing', 'price_components', 'derivatives', + 'fees', 'lots', 'product', 'currency', + } + + @classmethod + def _should_regenerate_valuation(cls, values): + return bool(cls._valuation_regeneration_fields() & set(values)) + @classmethod def write(cls, *args): actions = iter(args) args = [] + valuation_line_ids = set() for records, values in zip(actions, actions): cls._check_delivery_period_values(records, values) + if cls._should_regenerate_valuation(values): + valuation_line_ids.update(record.id for record in records) args.extend((records, values)) old_values = {} @@ -2684,9 +2796,18 @@ class SaleLine(metaclass=PoolMeta): Pool().get('lot.lot').assert_lines_quantity_consistency( cls._fresh_lines_for_quantity_consistency(lines)) + cls._create_missing_estimated_bl_dates(lines) if not Transaction().context.get('_purchase_trade_skip_fee_rules'): Pool().get('fee.rule').apply_to_lines( lines, 'sale_line', auto_only=True) + if (valuation_line_ids + and not Transaction().context.get( + '_purchase_trade_skip_valuation_regeneration')): + Valuation = Pool().get('valuation.valuation') + with Transaction().set_context( + _purchase_trade_skip_valuation_regeneration=True): + Valuation.regenerate_for_sale_lines( + cls.browse(list(valuation_line_ids))) @classmethod def _sync_open_lot_quantity(cls, line, vlot, target_quantity): @@ -3003,6 +3124,7 @@ class ValuationDyn(metaclass=PoolMeta): Sum(val.amount).as_('r_amount'), Sum(val.amount_prev).as_('r_amount_prev'), Sum(val.base_amount).as_('r_base_amount'), + Sum(val.pnl).as_('r_pnl'), Max(val.base_currency).as_('r_base_currency'), Sum(val.rate).as_('r_rate'), Avg(val.mtm_price).as_('r_mtm_price'), diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 7af826b..f10926e 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -9,7 +9,7 @@ from unittest.mock import ANY, Mock, call, patch from xml.etree import ElementTree from trytond.pool import Pool -from trytond.pyson import Eval +from trytond.pyson import Eval, PYSONDecoder from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.exceptions import UserError, UserWarning from trytond.transaction import Transaction @@ -902,6 +902,48 @@ class PurchaseTradeTestCase(ModuleTestCase): scenario.get_valuation_date(), datetime.date(2026, 6, 18)) + def test_itsa_mtm_scenario_defaults_calendar(self): + 'ITSA mtm scenarios default to the sulphuric acid calendar' + with patch('trytond.modules.purchase_trade.pricing.is_itsa_company', + return_value=True), patch( + 'trytond.modules.purchase_trade.pricing.' + 'default_itsa_price_calendar', + return_value=17): + self.assertEqual(pricing_module.MtmScenario.default_calendar(), 17) + + def test_itsa_mtm_component_defaults(self): + 'ITSA mtm components default market price, ratio and USD currency' + with patch('trytond.modules.purchase_trade.pricing.is_itsa_company', + return_value=True), patch( + 'trytond.modules.purchase_trade.pricing.' + 'default_itsa_price_fix_type', + return_value=21), patch( + 'trytond.modules.purchase_trade.pricing.default_itsa_currency', + return_value=840): + self.assertEqual(pricing_module.Mtm.default_fix_type(), 21) + self.assertEqual( + pricing_module.Mtm.default_ratio(), Decimal('100')) + self.assertEqual(pricing_module.Mtm.default_currency(), 840) + + def test_itsa_pricing_component_defaults(self): + 'ITSA pricing definitions default market price, ratio, USD and calendar' + with patch('trytond.modules.purchase_trade.pricing.is_itsa_company', + return_value=True), patch( + 'trytond.modules.purchase_trade.pricing.' + 'default_itsa_price_fix_type', + return_value=21), patch( + 'trytond.modules.purchase_trade.pricing.' + 'default_itsa_price_calendar', + return_value=17), patch( + 'trytond.modules.purchase_trade.pricing.default_itsa_currency', + return_value=840): + self.assertEqual(pricing_module.Component.default_fix_type(), 21) + self.assertEqual( + pricing_module.Component.default_ratio(), Decimal('100')) + self.assertEqual( + pricing_module.Component.default_fixed_currency(), 840) + self.assertEqual(pricing_module.Component.default_calendar(), 17) + def test_mtm_scenario_resolves_relative_valuation_dates(self): 'mtm scenario can resolve relative valuation dates' scenario = pricing_module.MtmScenario() @@ -1059,7 +1101,8 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(len(target), 3) self.assertEqual(target[0], values) - self.assertEqual(target[1]['type'], 'mtm') + self.assertEqual(target[0]['pnl'], Decimal('-100.00')) + self.assertEqual(target[1]['type'], 'pur. mtm') self.assertEqual(target[1]['mtm_curve'], curve_a.id) self.assertEqual(target[1]['price'], None) self.assertEqual(target[1]['amount'], Decimal('0')) @@ -1068,7 +1111,8 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(target[1]['mtm_price_prev'], Decimal('90.0000')) self.assertEqual(target[1]['amount_prev'], Decimal('-540.00')) self.assertEqual(target[1]['mtm'], Decimal('-600.00')) - self.assertEqual(target[2]['type'], 'mtm') + self.assertEqual(target[1]['pnl'], Decimal('600.00')) + self.assertEqual(target[2]['type'], 'pur. mtm') self.assertEqual(target[2]['mtm_curve'], curve_b.id) self.assertEqual(target[2]['price'], None) self.assertEqual(target[2]['amount'], Decimal('0')) @@ -1077,6 +1121,55 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(target[2]['mtm_price_prev'], Decimal('45.0000')) self.assertEqual(target[2]['amount_prev'], Decimal('-180.00')) self.assertEqual(target[2]['mtm'], Decimal('-200.00')) + self.assertEqual(target[2]['pnl'], Decimal('200.00')) + + @with_transaction() + def test_sale_strategy_mtm_lines_use_sale_type(self): + 'sale strategy MTM lines are labelled as sale MTM' + Valuation = Pool().get('valuation.valuation') + line = Mock(unit=Mock()) + strategy = Mock( + scenario=Mock(valuation_date=datetime.date(2026, 6, 5)), + currency=None, + components=[], + get_mtm=Mock(return_value=Decimal('250'))) + line.mtm = [strategy] + values = { + 'type': 'sale priced', + 'amount': Decimal('100'), + 'base_amount': Decimal('100'), + 'quantity': Decimal('10'), + } + target = [] + + Valuation._append_pnl_values(target, values, line) + + self.assertEqual(target[1]['type'], 'sale mtm') + self.assertEqual(target[1]['mtm'], Decimal('250')) + self.assertEqual(target[1]['pnl'], Decimal('-250.00')) + + def test_pnl_converts_strategy_mtm_to_base_currency(self): + 'valuation PnL compares base amount with MTM in base currency' + Valuation = valuation_module.Valuation + source_currency = Mock(id=1) + base_currency = Mock(id=2) + strategy = Mock(currency=source_currency, scenario=None) + values = { + 'base_amount': Decimal('100'), + 'base_currency': base_currency, + } + Currency = Mock() + Currency.compute.return_value = Decimal('90') + + with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock: + PoolMock.return_value.get.return_value = Currency + + pnl = Valuation._pnl_amount( + values, strategy=strategy, mtm_amount=Decimal('120')) + + Currency.compute.assert_called_once_with( + source_currency, Decimal('120'), base_currency) + self.assertEqual(pnl, Decimal('10.00')) def test_purchase_pnl_uses_partial_lotqt_match_quantity(self): 'open matched purchase and sale pnl use the matched lot.qt quantity' @@ -1547,6 +1640,28 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertFalse( valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line)) + def test_regenerate_for_purchase_lines_deduplicates_targets(self): + 'targeted valuation regeneration processes each purchase line once' + Valuation = Pool().get('valuation.valuation') + line = Mock(id=10) + + with patch.object(Valuation, 'generate') as generate: + Valuation.regenerate_for_purchase_lines([line, line]) + + generate.assert_called_once_with(line, valuation_type='all') + + def test_regenerate_for_sale_lines_deduplicates_targets(self): + 'targeted valuation regeneration processes each sale line once' + Valuation = Pool().get('valuation.valuation') + sale_line = Mock(id=20) + + with patch.object( + Valuation, 'generate_from_sale_line') as generate_from_sale_line: + Valuation.regenerate_for_sale_lines([sale_line, sale_line]) + + generate_from_sale_line.assert_called_once_with( + sale_line, valuation_type='all') + def test_parse_numbers_supports_inline_and_legacy_separators(self): 'parse_numbers keeps supporting inline entry and legacy separators' self.assertEqual( @@ -1567,6 +1682,8 @@ class PurchaseTradeTestCase(ModuleTestCase): {'derivative'}) self.assertIn('pur. priced', Valuation._get_generate_types('goods')) self.assertIn('mtm', Valuation._get_generate_types('goods')) + self.assertIn('pur. mtm', Valuation._get_generate_types('goods')) + self.assertIn('sale mtm', Valuation._get_generate_types('goods')) def test_filter_values_by_types_keeps_matching_entries_only(self): 'type filtering keeps only the requested valuation entries' @@ -3596,6 +3713,26 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertTrue(SaleLine.quantity.readonly) self.assertTrue(PurchaseLine.quantity.readonly) + def test_allow_modification_after_validation_line_readonly_rule(self): + 'configuration flag unlocks lines only in quotation state' + Sale = Pool().get('sale.sale') + Purchase = Pool().get('purchase.purchase') + + for model in (Sale, Purchase): + expression = model.lines.states['readonly'] + self.assertTrue(PYSONDecoder({ + 'state': 'quotation', + 'allow_modification_after_validation': False, + }).decode(expression)) + self.assertFalse(PYSONDecoder({ + 'state': 'quotation', + 'allow_modification_after_validation': True, + }).decode(expression)) + self.assertTrue(PYSONDecoder({ + 'state': 'confirmed', + 'allow_modification_after_validation': True, + }).decode(expression)) + def test_purchase_line_initial_quantity_uses_theoretical_quantity(self): 'purchase line initializes technical quantity from contractual quantity' PurchaseLine = Pool().get('purchase.line') @@ -4004,6 +4141,84 @@ class PurchaseTradeTestCase(ModuleTestCase): 'quantity': Decimal('1'), }) + def test_purchase_and_sale_line_create_default_estimated_bl_date(self): + 'new lines default missing estimated BL date from delivery period start' + PurchaseLine = Pool().get('purchase.line') + SaleLine = Pool().get('sale.line') + period = Mock(beg_date=datetime.date(2026, 5, 1)) + purchase_values = {'del_period': period} + sale_values = {'del_period': period} + + PurchaseLine._set_default_estimated_bl_date_values(purchase_values) + SaleLine._set_default_estimated_bl_date_values(sale_values) + + self.assertEqual(purchase_values['estimated_date'], [('create', [{ + 'trigger': 'bldate', + 'estimated_date': datetime.date(2026, 5, 1), + }])]) + self.assertEqual(sale_values['estimated_date'], [('create', [{ + 'trigger': 'bldate', + 'estimated_date': datetime.date(2026, 5, 1), + }])]) + + def test_purchase_and_sale_line_keep_explicit_estimated_bl_date(self): + 'new lines keep explicit estimated BL date' + PurchaseLine = Pool().get('purchase.line') + SaleLine = Pool().get('sale.line') + period = Mock(beg_date=datetime.date(2026, 5, 1)) + explicit = [('create', [{ + 'trigger': 'bldate', + 'estimated_date': datetime.date(2026, 5, 9), + }])] + purchase_values = {'del_period': period, 'estimated_date': list(explicit)} + sale_values = {'del_period': period, 'estimated_date': list(explicit)} + + PurchaseLine._set_default_estimated_bl_date_values(purchase_values) + SaleLine._set_default_estimated_bl_date_values(sale_values) + + self.assertEqual(purchase_values['estimated_date'], explicit) + self.assertEqual(sale_values['estimated_date'], explicit) + + def test_purchase_line_write_creates_missing_estimated_bl_date(self): + 'purchase line write creates missing estimated BL date from period' + PurchaseLine = Pool().get('purchase.line') + line = Mock( + id=42, + del_period=Mock(beg_date=datetime.date(2026, 5, 1)), + estimated_date=[], + ) + + with patch('trytond.modules.purchase_trade.purchase.Pool') as PoolMock: + Estimated = Mock() + PoolMock.return_value.get.return_value = Estimated + PurchaseLine._create_missing_estimated_bl_dates([line]) + + Estimated.create.assert_called_once_with([{ + 'line': 42, + 'trigger': 'bldate', + 'estimated_date': datetime.date(2026, 5, 1), + }]) + + def test_sale_line_write_creates_missing_estimated_bl_date(self): + 'sale line write creates missing estimated BL date from period' + SaleLine = Pool().get('sale.line') + line = Mock( + id=43, + del_period=Mock(beg_date=datetime.date(2026, 5, 1)), + estimated_date=[], + ) + + with patch('trytond.modules.purchase_trade.sale.Pool') as PoolMock: + Estimated = Mock() + PoolMock.return_value.get.return_value = Estimated + SaleLine._create_missing_estimated_bl_dates([line]) + + Estimated.create.assert_called_once_with([{ + 'sale_line': 43, + 'trigger': 'bldate', + 'estimated_date': datetime.date(2026, 5, 1), + }]) + def test_sale_and_purchase_parent_write_check_embedded_line_commands(self): 'sale and purchase writes validate embedded one2many line commands' Sale = Pool().get('sale.sale') diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index ab5b8fa..fe7840d 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -33,6 +33,8 @@ VALTYPE = [ ('shipment fee', 'Shipment fee'), ('market', 'Market'), ('mtm', 'Mtm'), + ('pur. mtm', 'Pur. Mtm'), + ('sale mtm', 'Sale Mtm'), ('derivative', 'Derivative'), ] @@ -67,6 +69,7 @@ class ValuationBase(ModelSQL): shipment_in = fields.Many2One('stock.shipment.in', "Shipment In") fee = fields.Many2One('fee.fee', "Fee") base_amount = fields.Numeric("Base Amount",digits=(16,2)) + pnl = fields.Numeric("Pnl", digits=(16, 2)) base_currency = fields.Many2One('currency.currency', "Base Cur") rate = fields.Numeric("Rate", digits=(16,6)) @@ -77,7 +80,8 @@ class ValuationBase(ModelSQL): 'fees': {'line fee', 'pur. fee', 'sale fee', 'shipment fee'}, 'goods': { 'priced', 'pur. priced', 'pur. efp', - 'sale priced', 'sale efp', 'market', 'mtm', + 'sale priced', 'sale efp', 'market', + 'mtm', 'pur. mtm', 'sale mtm', }, 'derivatives': {'derivative'}, } @@ -370,6 +374,12 @@ class ValuationBase(ModelSQL): seen.add(key) return unique + @classmethod + def _prepare_values_for_create(cls, values): + for value in values: + cls._set_pnl(value, strategy=value.get('strategy')) + return values + @classmethod def _snapshot_identity_domain(cls, value): domain = [ @@ -450,6 +460,30 @@ class ValuationBase(ModelSQL): def _sale_line_is_unmatched(cls, sale_line): return not cls._matched_purchase_lines_from_sale_line(sale_line) + @classmethod + def _unique_records(cls, records): + unique = [] + seen = set() + for record in records or []: + record_id = cls._record_id(record) + key = record_id if record_id is not None else id(record) + if not record or key in seen: + continue + unique.append(record) + seen.add(key) + return unique + + @classmethod + def regenerate_for_purchase_lines(cls, lines, valuation_type='all'): + for line in cls._unique_records(lines): + cls.generate(line, valuation_type=valuation_type) + + @classmethod + def regenerate_for_sale_lines(cls, sale_lines, valuation_type='all'): + for sale_line in cls._unique_records(sale_lines): + cls.generate_from_sale_line( + sale_line, valuation_type=valuation_type) + @classmethod def _delete_existing_sale_line(cls, sale_line, selected_types=None): Date = Pool().get('ir.date') @@ -569,6 +603,68 @@ class ValuationBase(ModelSQL): return -abs(mtm) return abs(mtm) + @staticmethod + def _strategy_mtm_type(values): + if values.get('type') in {'pur. priced', 'pur. efp'}: + return 'pur. mtm' + if values.get('type') in {'sale priced', 'sale efp'}: + return 'sale mtm' + return 'mtm' + + @staticmethod + def _coerce_decimal(value): + if value in (None, ''): + return Decimal(0) + return Decimal(str(value)) + + @classmethod + def _mtm_base_amount(cls, mtm_amount, strategy, values): + if mtm_amount in (None, ''): + return Decimal(0) + mtm_amount = Decimal(str(mtm_amount)) + source_currency = getattr(strategy, 'currency', None) + base_currency = values.get('base_currency') + if not source_currency or not base_currency: + return mtm_amount + + source_currency_id = cls._record_id(source_currency) + base_currency_id = cls._record_id(base_currency) + if source_currency_id == base_currency_id: + return mtm_amount + + Currency = Pool().get('currency.currency') + if isinstance(source_currency, int): + source_currency = Currency(source_currency) + if isinstance(base_currency, int): + base_currency = Currency(base_currency) + + scenario = getattr(strategy, 'scenario', None) + conversion_date = ( + cls._strategy_valuation_date(strategy) + if scenario else values.get('date')) + if conversion_date: + with Transaction().set_context(date=conversion_date): + return Currency.compute( + source_currency, mtm_amount, base_currency) + return Currency.compute(source_currency, mtm_amount, base_currency) + + @classmethod + def _pnl_amount(cls, values, strategy=None, mtm_amount=None): + base_amount = cls._coerce_decimal(values.get('base_amount')) + if mtm_amount is None: + mtm_amount = values.get('mtm') + mtm_base_amount = ( + cls._mtm_base_amount(mtm_amount, strategy, values) + if strategy else cls._coerce_decimal(mtm_amount)) + return round(base_amount - mtm_base_amount, 2) + + @classmethod + def _set_pnl(cls, values, strategy=None, mtm_amount=None): + if values is not None: + values['pnl'] = cls._pnl_amount( + values, strategy=strategy, mtm_amount=mtm_amount) + return values + @staticmethod def _component_weight(component): ratio = Decimal(component.ratio or 0) @@ -623,7 +719,7 @@ class ValuationBase(ModelSQL): mtm_curve=None, mtm_price_prev=None, mtm_amount_prev=None): line_values = dict(values) line_values.update({ - 'type': 'mtm', + 'type': cls._strategy_mtm_type(values), 'reference': 'MTM/%s' % (values.get('reference') or ''), 'price': None, 'amount': Decimal(0), @@ -635,6 +731,7 @@ class ValuationBase(ModelSQL): 'mtm': mtm_amount, 'strategy': strategy, }) + cls._set_pnl(line_values, strategy=strategy, mtm_amount=mtm_amount) return line_values @classmethod @@ -842,11 +939,13 @@ class ValuationBase(ModelSQL): def _append_pnl_values(cls, price_lines, values, mtm_source): if (values and getattr(mtm_source, 'mtm', None) and cls._supports_strategy_mtm(values)): + cls._set_pnl(values) price_lines.append(values) for strat in mtm_source.mtm: cls._append_strategy_mtm_lines( price_lines, values, strat, mtm_source) elif values: + cls._set_pnl(values) price_lines.append(values) @classmethod @@ -1467,6 +1566,7 @@ class ValuationBase(ModelSQL): values.extend(cls.create_pnl_der_from_line(line)) values = cls._filter_values_by_types(values, selected_types) values = cls._dedupe_values(values) + values = cls._prepare_values_for_create(values) cls._delete_existing_snapshot_values(values) if values: @@ -1492,6 +1592,7 @@ class ValuationBase(ModelSQL): values.extend(cls.create_pnl_der_from_sale_line(sale_line)) values = cls._filter_values_by_types(values, selected_types) values = cls._dedupe_values(values) + values = cls._prepare_values_for_create(values) cls._delete_existing_snapshot_values(values) if values: @@ -1589,6 +1690,7 @@ class ValuationDyn(ModelSQL,ModelView): r_amount = fields.Numeric("Amount",digits='r_unit') r_amount_prev = fields.Numeric("Amount -1", digits='r_unit') r_base_amount = fields.Numeric("Base Amount",digits='r_unit') + r_pnl = fields.Numeric("Pnl", digits='r_unit') r_base_currency = fields.Many2One('currency.currency', "Base Cur") r_rate = fields.Numeric("Rate",digits=(16,6)) r_mtm_price = fields.Numeric("Mtm Price",digits='r_unit') @@ -1628,6 +1730,7 @@ class ValuationDyn(ModelSQL,ModelView): Sum(val.amount).as_('r_amount'), Sum(val.amount_prev).as_('r_amount_prev'), Sum(val.base_amount).as_('r_base_amount'), + Sum(val.pnl).as_('r_pnl'), Max(val.base_currency).as_('r_base_currency'), Sum(val.rate).as_('r_rate'), Avg(val.mtm_price).as_('r_mtm_price'), @@ -1686,6 +1789,7 @@ class ValuationReport(ValuationBase, ModelView): val.amount.as_('amount'), val.amount_prev.as_('amount_prev'), val.base_amount.as_('base_amount'), + val.pnl.as_('pnl'), val.base_currency.as_('base_currency'), val.rate.as_('rate'), val.mtm_price.as_('mtm_price'), diff --git a/modules/purchase_trade/view/purchase_configuration_form.xml b/modules/purchase_trade/view/purchase_configuration_form.xml new file mode 100644 index 0000000..2d8995d --- /dev/null +++ b/modules/purchase_trade/view/purchase_configuration_form.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/modules/purchase_trade/view/purchase_form.xml b/modules/purchase_trade/view/purchase_form.xml index 32f2f59..5c7f729 100755 --- a/modules/purchase_trade/view/purchase_form.xml +++ b/modules/purchase_trade/view/purchase_form.xml @@ -3,6 +3,9 @@ this repository contains the full copyright notices and license terms. --> + + + + + + + diff --git a/modules/purchase_trade/view/sale_form.xml b/modules/purchase_trade/view/sale_form.xml index 46749a8..9aecdb3 100755 --- a/modules/purchase_trade/view/sale_form.xml +++ b/modules/purchase_trade/view/sale_form.xml @@ -3,6 +3,9 @@ this repository contains the full copyright notices and license terms. --> + + + + badge_colors="priced:#2563eb,Price:#2563eb,pur. priced:#1d4ed8,Pur. price:#1d4ed8,pur. efp:#38bdf8,Pur. efp:#38bdf8,sale priced:#16a34a,Sale price:#16a34a,sale efp:#86efac,Sale efp:#86efac,market:#64748b,Market:#64748b,line fee:#d97706,Line fee:#d97706,pur. fee:#f59e0b,Pur. fee:#f59e0b,sale fee:#fb923c,Sale fee:#fb923c,shipment fee:#0f766e,Shipment fee:#0f766e,mtm:#9f1239,Mtm:#9f1239,pur. mtm:#9f1239,Pur. Mtm:#9f1239,sale mtm:#9f1239,Sale Mtm:#9f1239,derivative:#8b5cf6,Derivative:#8b5cf6,*:#94a3b8"/> @@ -19,6 +19,7 @@ + diff --git a/modules/purchase_trade/view/valuation_tree_sequence3.xml b/modules/purchase_trade/view/valuation_tree_sequence3.xml index 3d8bc51..c5ac8e7 100755 --- a/modules/purchase_trade/view/valuation_tree_sequence3.xml +++ b/modules/purchase_trade/view/valuation_tree_sequence3.xml @@ -7,7 +7,7 @@ this repository contains the full copyright notices and license terms. --> + badge_colors="priced:#2563eb,Price:#2563eb,pur. priced:#1d4ed8,Pur. price:#1d4ed8,pur. efp:#38bdf8,Pur. efp:#38bdf8,sale priced:#16a34a,Sale price:#16a34a,sale efp:#86efac,Sale efp:#86efac,market:#64748b,Market:#64748b,line fee:#d97706,Line fee:#d97706,pur. fee:#f59e0b,Pur. fee:#f59e0b,sale fee:#fb923c,Sale fee:#fb923c,shipment fee:#0f766e,Shipment fee:#0f766e,mtm:#9f1239,Mtm:#9f1239,pur. mtm:#9f1239,Pur. Mtm:#9f1239,sale mtm:#9f1239,Sale Mtm:#9f1239,derivative:#8b5cf6,Derivative:#8b5cf6,*:#94a3b8"/> @@ -20,6 +20,7 @@ this repository contains the full copyright notices and license terms. --> + diff --git a/modules/purchase_trade/view/valuation_tree_sequence4.xml b/modules/purchase_trade/view/valuation_tree_sequence4.xml index 0070c03..ef537e7 100755 --- a/modules/purchase_trade/view/valuation_tree_sequence4.xml +++ b/modules/purchase_trade/view/valuation_tree_sequence4.xml @@ -15,6 +15,7 @@ this repository contains the full copyright notices and license terms. --> sum_previous="r_amount_prev" sum_variation="1"/> +