diff --git a/modules/purchase_trade/AGENTS.md b/modules/purchase_trade/AGENTS.md
index 8a00366..7489422 100644
--- a/modules/purchase_trade/AGENTS.md
+++ b/modules/purchase_trade/AGENTS.md
@@ -79,15 +79,27 @@ de negoce physique:
- `mtm_price` doit afficher le prix brut de valorisation (sans ratio), alors
que `mtm` reste le montant calcule selon la logique de strategie
- En pricing:
- - le `premium` doit impacter le prix total et donc le `amount`, aussi bien
- en `priced` qu'en `basis`
+ - le `unit_price` doit rester un prix de base, hors `premium`
+ - le `premium` doit impacter le prix total economique et donc le `amount`,
+ aussi bien en `priced` qu'en `basis`
- si `linked currency` est active, le `premium` est saisi dans la devise /
- unite liee (ex: `USC/LB`) puis converti vers le `unit_price` de la ligne
+ unite liee (ex: `USC/LB`) puis converti vers le repere de la ligne pour le
+ calcul du `amount`
- en `basis + linked currency`, le `linked_price` doit representer le prix
- basis brut (hors premium) dans la devise liee; le `unit_price` final
- ajoute ensuite le premium converti
+ basis brut (hors premium) dans la devise liee; le `unit_price` reste ce
+ prix brut converti, et le `premium` converti est ajoute seulement dans
+ l'`amount`
+ - si `linked currency` est cochee, `linked_price`, `linked_currency` et
+ `linked_unit` sont requis
+ - dans les forms, presenter le bloc prix dans l'ordre:
+ `price_type` -> linked fields -> `premium` -> `unit_price` -> `amount`
- en valuation `basis`, le premium s'applique a chaque composant, pas
uniquement a une ligne de resume
+ - pour une ligne `basis` sans `price_summary`, la valuation fallback doit
+ utiliser `unit_price + premium` (et pas `unit_price` seul)
+ - a la validation d'une `sale.line`, si un lot virtuel est cree et qu'aucun
+ matching purchase n'existe, il faut lancer `generate_from_sale_line()` pour
+ alimenter le PnL sale-first
## 5) Conventions de modification
diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md
index 98e0b69..0c0ea9b 100644
--- a/modules/purchase_trade/docs/business-rules.md
+++ b/modules/purchase_trade/docs/business-rules.md
@@ -199,12 +199,12 @@ Owner technique: `a completer`
- Le `premium` d'une `purchase.line` ou `sale.line` doit impacter le prix
total quelle que soit la `price_type`.
- Cette regle vaut pour:
- - le `unit_price`
- les calculs de `amount`
- la valuation / PnL
- Resultat attendu:
- - en `priced`, le prix total = prix de base + premium
- - en `basis`, le premium s'ajoute aussi au prix total
+ - le `unit_price` reste le prix de base, hors premium
+ - en `priced`, le montant economique = `unit_price + premium`
+ - en `basis`, le premium s'ajoute aussi au prix total economique
- en valuation `basis`, le premium s'applique a chaque composant valorise
(ex: meme premium repete sur chaque bloc ICE)
- Exemple metier:
@@ -221,13 +221,14 @@ Owner technique: `a completer`
- Description:
- Quand `enable_linked_currency` est coche, le `premium` est saisi dans la
devise / unite liee, pas dans la devise / unite native de la ligne.
- - Le systeme doit convertir ce premium vers le `unit_price` de la ligne pour
- les calculs internes.
+ - Le systeme doit convertir ce premium vers le repere de la ligne pour les
+ calculs internes de montant et de valuation.
- Resultat attendu:
- `premium` est interprete dans le repere `linked_currency` / `linked_unit`
- - le `unit_price` converti integre ce premium
- - les `amount` et valuations bases sur `unit_price` reflectent donc ce
- premium converti
+ - le `unit_price` ne doit pas absorber ce premium
+ - les `amount` et valuations doivent refleter ce premium converti
+ - si `linked currency` est cochee, `linked_price`, `linked_currency` et
+ `linked_unit` sont obligatoires
- Priorite:
- `structurante`
@@ -238,12 +239,47 @@ Owner technique: `a completer`
- Quand une ligne est en `basis` et `linked currency`, le bloc
`linked_price` doit etre recalcule automatiquement.
- Ce `linked_price` doit representer le prix basis brut, hors premium.
- - Le `unit_price` final de la ligne est ensuite obtenu en ajoutant le premium
- converti.
+ - Le `unit_price` de la ligne doit rester ce prix brut converti.
+ - Le premium converti n'est ajoute qu'au niveau du `amount`.
- Resultat attendu:
- modification du basis -> mise a jour automatique du `linked_price`
- `linked_price` = base market / basis
- - `unit_price` = `linked_price` converti + premium converti
+ - `unit_price` = `linked_price` converti
+ - `amount` = quantite * (`unit_price` + premium converti)
+- Priorite:
+ - `importante`
+
+### BR-PT-011 - Une sale line non matchee avec lot virtuel doit generer une valuation sale-first des la validation
+
+- Intent: ne pas attendre un matching purchase pour afficher le PnL d'une sale
+ ouverte.
+- Description:
+ - Lors de la validation d'une `sale.line`, le systeme peut creer un lot
+ `virtual`.
+ - Si aucun `lot.qt` ne relie ce lot a une `purchase.line`, il faut tout de
+ meme generer la valuation cote sale.
+- Resultat attendu:
+ - apres creation du lot virtuel, si aucun matching purchase n'existe:
+ - appeler `Valuation.generate_from_sale_line(line)`
+ - creer au moins la ligne `sale priced` fallback si la ligne porte un prix
+ economique via le premium
+- Priorite:
+ - `importante`
+
+### BR-PT-012 - Fallback valuation basis sans summary: utiliser le prix economique de la ligne
+
+- Intent: eviter qu'une valuation `basis` ouverte sorte a zero alors que la
+ ligne a bien une valeur economique via le premium.
+- Description:
+ - Une ligne `basis` peut ne pas avoir encore de `price_summary`.
+ - Dans ce cas, la valuation fallback ne doit pas prendre `unit_price` seul si
+ celui-ci est brut et hors premium.
+- Resultat attendu:
+ - le fallback valuation `basis` doit utiliser:
+ - `unit_price + premium converti`
+ - cette regle vaut au minimum pour:
+ - `sale.line` non matchee
+ - `purchase.line` sans summary
- Priorite:
- `importante`
diff --git a/modules/purchase_trade/docs/template-properties.md b/modules/purchase_trade/docs/template-properties.md
index ea020d6..abb272f 100644
--- a/modules/purchase_trade/docs/template-properties.md
+++ b/modules/purchase_trade/docs/template-properties.md
@@ -239,6 +239,7 @@ Source code: `modules/purchase_trade/invoice.py`
Source code: `modules/purchase_trade/sale.py`
- `report_terms`
+- `report_crop_name`
- `report_gross`
- `report_net`
- `report_qt`
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index 6378307..92e53e6 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -352,19 +352,25 @@ class Sale(metaclass=PoolMeta):
return ''
@property
- def report_nb_bale(self):
- text_bale = 'NB BALES: '
- nb_bale = 0
- if self.lines:
- for line in self.lines:
- if line.lots:
- nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
- return text_bale + str(int(nb_bale))
-
- @property
- def report_deal(self):
- if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1:
- return self.lines[0].lots[1].line.purchase.number + ' ' + self.number
+ def report_nb_bale(self):
+ text_bale = 'NB BALES: '
+ nb_bale = 0
+ if self.lines:
+ for line in self.lines:
+ if line.lots:
+ nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
+ return text_bale + str(int(nb_bale))
+
+ @property
+ def report_crop_name(self):
+ if self.crop:
+ return self.crop.name or ''
+ return ''
+
+ @property
+ def report_deal(self):
+ if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1:
+ return self.lines[0].lots[1].line.purchase.number + ' ' + self.number
else:
''
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index 9313ef3..f3ffece 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -142,5 +142,17 @@ class PurchaseTradeTestCase(ModuleTestCase):
{'type': 'derivative', 'amount': Decimal('30')},
])
+ def test_sale_report_crop_name_handles_missing_crop(self):
+ 'sale report crop name returns an empty string when crop is missing'
+ Sale = Pool().get('sale.sale')
+
+ sale = Sale()
+ sale.crop = None
+ self.assertEqual(sale.report_crop_name, '')
+
+ sale.crop = Mock(name='crop')
+ sale.crop.name = 'Main Crop'
+ self.assertEqual(sale.report_crop_name, 'Main Crop')
+
del ModuleTestCase
diff --git a/modules/sale/sale_ict.fodt b/modules/sale/sale_ict.fodt
index 1627c8c..e89e81c 100644
--- a/modules/sale/sale_ict.fodt
+++ b/modules/sale/sale_ict.fodt
@@ -4069,7 +4069,7 @@
CROP
- <sale.crop.name>
+ <sale.report_crop_name>