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.