From 52459a267b75b5f35fafb81c66fbf47e60db994a Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 11 Jun 2026 10:49:05 +0200 Subject: [PATCH] Pricing manual --- modules/purchase_trade/docs/business-rules.md | 4 +- .../purchase_trade/docs/business/pricing.md | 2 +- .../docs_source/business/pricing.md | 2 +- modules/purchase_trade/pricing.py | 57 ++++++++++++------- modules/purchase_trade/tests/test_module.py | 39 +++++++++++++ notes/template_business_rules.md | 3 +- 6 files changed, 84 insertions(+), 23 deletions(-) diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index 74d0d91..b1fe41f 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -418,7 +418,9 @@ Owner technique: `a completer` - `fixed_qt` = cumul des `quantity` - `fixed_qt_price` = moyenne ponderee cumulee des `settl_price` - `unfixed_qt` = quantite de base de la ligne - `fixed_qt` - - `unfixed_qt_price` = `settl_price` de la ligne + - `unfixed_qt_price` = dernier prix disponible de la courbe du composant + quand le composant est lie a une courbe; sinon fallback sur `settl_price` + de la ligne - `eod_price` = moyenne ponderee entre jambe fixee et non fixee - `last=True` reste unique par groupe et suit la plus grande `pricing_date` - Hors scope: diff --git a/modules/purchase_trade/docs/business/pricing.md b/modules/purchase_trade/docs/business/pricing.md index 46ac4d2..683752a 100644 --- a/modules/purchase_trade/docs/business/pricing.md +++ b/modules/purchase_trade/docs/business/pricing.md @@ -197,7 +197,7 @@ Source: `BR-PT-016` et doublon historique `BR-PT-015` unfixed_qt_price -Prix de fallback du solde non fixe. +Dernier prix disponible de la courbe du composant pour le solde non fixe, avec fallback sur settl_price si aucune courbe exploitable n'est disponible. eod_price diff --git a/modules/purchase_trade/docs_source/business/pricing.md b/modules/purchase_trade/docs_source/business/pricing.md index 18fbeec..cdbe5c5 100644 --- a/modules/purchase_trade/docs_source/business/pricing.md +++ b/modules/purchase_trade/docs_source/business/pricing.md @@ -91,7 +91,7 @@ Source: `BR-PT-016` et doublon historique `BR-PT-015` | `fixed_qt` | Cumul des quantites fixees. | | `fixed_qt_price` | Prix moyen pondere du cumul fixe. | | `unfixed_qt` | Solde restant a fixer. | -| `unfixed_qt_price` | Prix de fallback du solde non fixe. | +| `unfixed_qt_price` | Dernier prix disponible de la courbe du composant pour le solde non fixe, avec fallback sur `settl_price` si aucune courbe exploitable n'est disponible. | | `eod_price` | Prix economique courant de la ligne de fixing. | | `last` | Derniere ligne du groupe, utilisee par le summary. | diff --git a/modules/purchase_trade/pricing.py b/modules/purchase_trade/pricing.py index 7149590..1ebcf6f 100755 --- a/modules/purchase_trade/pricing.py +++ b/modules/purchase_trade/pricing.py @@ -1180,22 +1180,40 @@ class Pricing(ModelSQL,ModelView): return None @classmethod - def _get_base_quantity(cls, record): - owner = getattr(record, 'sale_line', None) or getattr(record, 'line', None) - if not owner: - return Decimal(0) - if hasattr(owner, '_get_pricing_base_quantity'): + def _get_base_quantity(cls, record): + owner = getattr(record, 'sale_line', None) or getattr(record, 'line', None) + if not owner: + return Decimal(0) + if hasattr(owner, '_get_pricing_base_quantity'): return Decimal(str(owner._get_pricing_base_quantity() or 0)) quantity = getattr(owner, 'quantity_theorical', None) if quantity is None: - quantity = getattr(owner, 'quantity', None) - return Decimal(str(quantity or 0)) - - @classmethod - def _sync_manual_values(cls, records): - if (not records - or Transaction().context.get('skip_pricing_manual_sync')): - return + quantity = getattr(owner, 'quantity', None) + return Decimal(str(quantity or 0)) + + @classmethod + def _get_manual_unfixed_price(cls, pricing, settl_price): + component = getattr(pricing, 'price_component', None) + if (not component + or getattr(component, 'price_source_type', None) != 'curve' + or not getattr(component, 'price_index', None)): + return settl_price + owner = getattr(pricing, 'sale_line', None) or getattr(pricing, 'line', None) + if not owner: + return settl_price + document = getattr(owner, 'sale', None) or getattr(owner, 'purchase', None) + currency = getattr(document, 'currency', None) + unit = getattr(owner, 'unit', None) + if not currency or not unit: + return settl_price + return round(Decimal(str(component.get_price( + pricing.pricing_date, unit, currency, True) or 0)), 4) + + @classmethod + def _sync_manual_values(cls, records): + if (not records + or Transaction().context.get('skip_pricing_manual_sync')): + return domains = [] seen = set() for record in records: @@ -1240,12 +1258,13 @@ class Pricing(ModelSQL,ModelView): unfixed_qt = Decimal(0) fixed_qt = base_quantity values = { - 'fixed_qt': fixed_qt, - 'fixed_qt_price': fixed_qt_price, - 'unfixed_qt': unfixed_qt, - 'unfixed_qt_price': settl_price, - 'last': index == (total - 1), - } + 'fixed_qt': fixed_qt, + 'fixed_qt_price': fixed_qt_price, + 'unfixed_qt': unfixed_qt, + 'unfixed_qt_price': cls._get_manual_unfixed_price( + pricing, settl_price), + 'last': index == (total - 1), + } eod_price = cls._weighted_average_price( values['fixed_qt'], values['fixed_qt_price'], diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 4ae6a9c..8fcb052 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2820,6 +2820,45 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(second_values['eod_price'], Decimal('106.0000')) self.assertTrue(second_values['last']) + def test_pricing_sync_manual_values_uses_curve_last_price_for_unfixed(self): + 'manual pricing rows use the component curve latest price for the unfixed leg' + Pricing = Pool().get('pricing.pricing') + + currency = Mock() + unit = Mock() + sale_line = Mock( + id=10, + sale=Mock(currency=currency), + unit=unit, + ) + sale_line._get_pricing_base_quantity = Mock(return_value=Decimal('10')) + component = Mock( + id=33, + auto=False, + price_source_type='curve', + price_index=Mock(), + ) + component.get_price.return_value = Decimal('120') + pricing = Mock( + id=1, + price_component=component, + sale_line=sale_line, + line=None, + pricing_date=datetime.date(2026, 4, 10), + quantity=Decimal('4'), + settl_price=Decimal('100'), + ) + + with patch.object(Pricing, 'search', return_value=[pricing]), patch( + 'trytond.modules.purchase_trade.pricing.super') as super_mock: + Pricing._sync_manual_values([pricing]) + + values = super_mock.return_value.write.call_args.args[1] + self.assertEqual(values['unfixed_qt_price'], Decimal('120.0000')) + self.assertEqual(values['eod_price'], Decimal('112.0000')) + component.get_price.assert_called_once_with( + datetime.date(2026, 4, 10), unit, currency, True) + def test_pricing_trigger_manual_from_to_generates_dates_without_calendar(self): 'manual pricing From/To uses calendar days when no calendar is selected' Trigger = Pool().get('pricing.trigger') diff --git a/notes/template_business_rules.md b/notes/template_business_rules.md index b0aa37d..290e568 100644 --- a/notes/template_business_rules.md +++ b/notes/template_business_rules.md @@ -90,7 +90,8 @@ Scope: templates Relatorio + ponts `report_*` Python. - `fixed_qt` = cumul des `quantity` du groupe. - `fixed_qt_price` = moyenne ponderee cumulee des `settl_price`. - `unfixed_qt` = quantite de base restante a fixer. - - `unfixed_qt_price` = `settl_price` de la ligne. + - `unfixed_qt_price` = dernier prix disponible de la courbe du composant, + fallback `settl_price` si aucune courbe exploitable n'est disponible. - `eod_price` reste non editable et suit le prix moyen pondere. - le mode auto suit la meme formule. - `last` est gere par groupe metier (`line + component`), avec un seul `last=True` par groupe.