diff --git a/modules/purchase_trade/AGENTS.md b/modules/purchase_trade/AGENTS.md
index 31c03c9..8a00366 100644
--- a/modules/purchase_trade/AGENTS.md
+++ b/modules/purchase_trade/AGENTS.md
@@ -49,6 +49,8 @@ de negoce physique:
## 4) Invariants metier a preserver
- Un lot `virtual` est la reference d'ouverture de quantite pour une `purchase.line`.
+- Une `sale.line` doit aussi avoir au minimum un lot `virtual`; une valuation
+ cote sale ne doit donc pas disparaitre juste parce que le lot est `open`.
- Le lot physique est le pont principal entre:
- `purchase.line`
- `sale.line`
@@ -59,6 +61,33 @@ de negoce physique:
- ne pas multiplier des chemins d'acces concurrents
- Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment
dont le produit est `Maritime freight`.
+- En valuation / PnL:
+ - la valeur stockee dans `type` est la cle technique (`pur. priced`,
+ `sale priced`, `pur. fee`, etc.), pas le label affiche dans l'UI
+ - les references doivent rester coherentes avec le type de lot:
+ `Purchase/Open`, `Purchase/Physic`, `Sale/Open`, `Sale/Physic`
+ - pour une sale matchee, les lignes de valuation purchase generees sur un lot
+ physique doivent aussi renseigner `sale` et `sale_line` afin de remonter
+ dans l'onglet PnL de la sale
+ - une sale non matchee doit etre valorisable "sale-first" et alimenter
+ `valuation.valuation` / `valuation.valuation.line`
+ - si une `sale.line` `basis` n'a ni `price_summary` ni `lot_price_sale`,
+ creer quand meme une ligne `sale priced` avec `price = 0` et `amount = 0`
+ plutot que de ne rien generer
+ - le MTM ne doit etre renseigne que pour `pur. priced`, `sale priced` et
+ `derivative`; jamais pour les fees
+ - `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`
+ - 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
+ - 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
+ - en valuation `basis`, le premium s'applique a chaque composant, pas
+ uniquement a une ligne de resume
## 5) Conventions de modification
@@ -83,6 +112,13 @@ de negoce physique:
- Pour les donnees achat/vente partagees, ne pas supposer qu'une facture de
vente doit lire directement sur la `sale.line`: souvent, la verite metier
passe par le lot physique et/ou la `account.invoice.line`.
+- Dans les ecrans PnL, le label `Sale price` correspond au type stocke
+ `sale priced`; idem pour `Pur. price` / `pur. priced`.
+- Une ligne `basis` sans resume de pricing peut sinon disparaitre de la
+ valuation si aucun fallback explicite a `0` n'est prevu.
+- Le calcul du prix peut diverger entre `unit_price`, `linked_price`,
+ `lot_price` et valuation si le premium n'est pas traite explicitement dans
+ chaque maillon.
## 7) Definition of done (module `purchase_trade`)
diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md
index 3095758..98e0b69 100644
--- a/modules/purchase_trade/docs/business-rules.md
+++ b/modules/purchase_trade/docs/business-rules.md
@@ -1,8 +1,8 @@
# Business Rules - Purchase Trade
Statut: `draft`
-Version: `v0.2`
-Derniere mise a jour: `2026-03-27`
+Version: `v0.3`
+Derniere mise a jour: `2026-04-01`
Owner metier: `a completer`
Owner technique: `a completer`
@@ -100,6 +100,153 @@ Owner technique: `a completer`
- Priorite:
- `importante`
+### BR-PT-004 - La valuation doit couvrir les flux purchase et sale, y compris les sales non matchees
+
+- Intent: obtenir un PnL coherent cote achat et cote vente, meme lorsqu'une
+ sale n'est pas encore matchee a une purchase.
+- Description:
+ - Le flux historique de valuation part de `purchase.line` puis remonte vers
+ les ventes via les lots/lots matchants.
+ - Le systeme doit egalement savoir valoriser directement une `sale.line`
+ non matchee ("sale-first").
+ - Une sale non matchee doit creer des lignes dans
+ `valuation.valuation` et `valuation.valuation.line` afin d'apparaitre dans
+ l'onglet PnL de la sale.
+- Resultat attendu:
+ - pour une `sale.line` non matchee, generer au minimum les types:
+ - `sale priced`
+ - `sale fee`
+ - `derivative` si la ligne porte des derives
+ - si la sale est matchee via un lot physique, les lignes purchase portees par
+ ce lot physique doivent aussi renseigner `sale` et `sale_line`
+ - une sale matchee doit donc voir:
+ - ses lignes `sale *`
+ - les lignes purchase portees par le lot physique partage
+- Priorite:
+ - `structurante`
+
+### BR-PT-005 - Les references de valuation doivent decrire la nature du lot de la ligne
+
+- Intent: eviter les ambiguïtes dans les ecrans PnL entre lots `open` et lots
+ `physic`.
+- Description:
+ - La reference affichee dans la valuation doit decrire la ligne elle-meme,
+ pas son vis-a-vis.
+ - Les references autorisees pour les lignes de prix sont:
+ - `Purchase/Open`
+ - `Purchase/Physic`
+ - `Sale/Open`
+ - `Sale/Physic`
+- Resultat attendu:
+ - un lot `virtual` cote purchase ne doit jamais sortir avec la reference
+ `Purchase/Physic`
+ - un lot `virtual` cote sale ne doit jamais sortir avec la reference
+ `Sale/Physic`
+ - un lot physique matche peut produire:
+ - une ligne purchase en `Purchase/Physic`
+ - une ligne sale en `Sale/Physic`
+ - un open sale matche a un open purchase peut produire des quantites egales
+ tout en gardant des references differentes (`Purchase/Open` vs `Sale/Open`)
+- Priorite:
+ - `importante`
+
+### BR-PT-006 - Une sale basis sans prix detaille doit quand meme apparaitre en valuation
+
+- Intent: ne pas perdre les lignes de PnL lorsque le detail de pricing n'est
+ pas encore renseigne.
+- Description:
+ - Une `sale.line` de type `basis` peut exister avec un lot `virtual`, sans
+ `price_summary` et sans `lot_price_sale`.
+ - Dans ce cas, la valuation doit quand meme creer une ligne `sale priced`.
+- Resultat attendu:
+ - si `price_summary` est vide:
+ - creer une ligne `sale priced`
+ - avec `price = 0`
+ - avec `amount = 0`
+ - avec un `state` de type `unfixed`
+ - si `lot_price_sale` est vide sur un lot sale, utiliser `sale_line.unit_price`
+ comme fallback quand il existe
+- Priorite:
+ - `importante`
+
+### BR-PT-007 - Le MTM de valuation ne s'applique pas aux fees
+
+- Intent: distinguer les lignes de prix marquables au marche des lignes de
+ frais qui ne doivent pas etre mark-to-market.
+- Description:
+ - Le systeme peut renseigner `mtm_price`, `mtm` et `strategy` uniquement pour:
+ - `pur. priced`
+ - `sale priced`
+ - `derivative`
+ - Les fees (`pur. fee`, `sale fee`, `shipment fee`, `line fee`) ne doivent
+ jamais porter de valorisation MTM.
+- Resultat attendu:
+ - les lignes de fee doivent conserver:
+ - `mtm_price = NULL`
+ - `mtm = NULL`
+ - `strategy = NULL`
+ - `mtm_price` doit representer le prix brut de valorisation sans appliquer le
+ ratio de composant
+ - `mtm` reste le montant calcule selon la logique de strategie
+- Priorite:
+ - `structurante`
+
+### BR-PT-008 - Le premium fait partie du prix contractuel en `priced` et en `basis`
+
+- Intent: garantir que le montant total valorise et facture reflete toujours le
+ premium/discount saisi sur la ligne.
+- Description:
+ - 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
+ - en valuation `basis`, le premium s'applique a chaque composant valorise
+ (ex: meme premium repete sur chaque bloc ICE)
+- Exemple metier:
+ - `8.30 USC/LB 500 TONS ON ICE MCH'26`
+ - `8.30 USC/LB 500 TONS ON ICE MAY 26`
+ - le premium `8.30 USC/LB` s'applique a chaque composant
+- Priorite:
+ - `structurante`
+
+### BR-PT-009 - En linked currency, le premium est exprime dans la devise/unite liee
+
+- Intent: respecter la facon dont les traders saisissent les prix sur certains
+ produits (ex: coton en `USC/LB`).
+- 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.
+- 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
+- Priorite:
+ - `structurante`
+
+### BR-PT-010 - En `basis + linked currency`, le linked price suit le basis brut
+
+- Intent: rendre lisible la decomposition entre prix basis de marche et premium.
+- Description:
+ - 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.
+- Resultat attendu:
+ - modification du basis -> mise a jour automatique du `linked_price`
+ - `linked_price` = base market / basis
+ - `unit_price` = `linked_price` converti + premium converti
+- Priorite:
+ - `importante`
+
## 4) Exemples concrets
### Exemple E1 - Augmentation simple
@@ -146,6 +293,8 @@ Owner technique: `a completer`
- Fichiers Python concernes:
- `modules/purchase_trade/purchase.py`
- `modules/purchase_trade/lot.py`
+ - `modules/purchase_trade/valuation.py`
+ - `modules/purchase_trade/sale.py`
## 6) Strategie de tests
@@ -155,3 +304,11 @@ Pour cette regle, couvrir au minimum:
- augmentation sans `lot.qt` ouvert
- diminution possible
- diminution impossible avec erreur
+- valuation purchase/sale sur lot physique matche
+- valuation sale-first sur sale non matchee avec lot virtual
+- valuation sale `basis` sans `price_summary`
+- absence de MTM sur les fees
+- premium en `priced`
+- premium en `basis`
+- premium en `linked currency`
+- synchro `basis` -> `linked_price` -> `unit_price`
diff --git a/modules/purchase_trade/docs/template-rules.md b/modules/purchase_trade/docs/template-rules.md
index 947a35b..1a140ce 100644
--- a/modules/purchase_trade/docs/template-rules.md
+++ b/modules/purchase_trade/docs/template-rules.md
@@ -126,6 +126,18 @@ Derniere mise a jour: `2026-03-27`
- utiliser `fee.get_amount()`
- Si le fee a sa propre devise, preferer aussi exposer le symbole de devise depuis le fee plutot que depuis la facture.
+### TR-009 - Ne pas dereferencer directement `del_period.description` dans les templates
+
+- Eviter les expressions du type:
+ - `sale.lines[0].del_period.description`
+ - `purchase.lines[0].del_period.description`
+- Meme avec un `if ... else`, ces acces sont fragiles dans un `.fodt` et
+ rendent le debug plus difficile.
+- Preferer une propriete Python stable:
+ - `sale.report_delivery_period_description`
+ - `purchase.report_delivery_period_description`
+ - `invoice.report_delivery_period_description`
+
## 4) Workflow recommande pour corriger un template en erreur
1. Identifier le placeholder exact qui provoque l'erreur Relatorio.
diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py
index ea11701..65d2e97 100644
--- a/modules/purchase_trade/invoice.py
+++ b/modules/purchase_trade/invoice.py
@@ -188,6 +188,16 @@ class Invoice(metaclass=PoolMeta):
return trade.report_payment_date
return ''
+ @property
+ def report_delivery_period_description(self):
+ trade = self._get_report_trade()
+ if trade and getattr(trade, 'report_delivery_period_description', None):
+ return trade.report_delivery_period_description
+ line = self._get_report_trade_line()
+ if line and getattr(line, 'del_period', None):
+ return line.del_period.description or ''
+ return ''
+
@property
def report_payment_description(self):
trade = self._get_report_trade()
diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py
index 4b9f9fa..1c55e16 100755
--- a/modules/purchase_trade/purchase.py
+++ b/modules/purchase_trade/purchase.py
@@ -384,16 +384,22 @@ class Purchase(metaclass=PoolMeta):
return ''
@property
- def report_delivery(self):
- del_date = 'PROMPT'
- if self.lines:
- if self.lines[0].estimated_date:
- delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
- if delivery_date:
- del_date = delivery_date[0]
- if del_date:
- del_date = format_date_en(del_date)
- return del_date
+ def report_delivery(self):
+ del_date = 'PROMPT'
+ if self.lines:
+ if self.lines[0].estimated_date:
+ delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
+ if delivery_date:
+ del_date = delivery_date[0]
+ if del_date:
+ del_date = format_date_en(del_date)
+ return del_date
+
+ @property
+ def report_delivery_period_description(self):
+ if self.lines and self.lines[0].del_period:
+ return self.lines[0].del_period.description or ''
+ return ''
@property
def report_payment_date(self):
@@ -543,14 +549,16 @@ class Purchase(metaclass=PoolMeta):
OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_purchase_line(line)
- #line unit_price calculation
- if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
- unit_price = line.get_basis_price()
- logger.info("VALIDATEPURCHASE:%s",unit_price)
- if unit_price != line.unit_price:
- line.unit_price = unit_price
- logger.info("VALIDATEPURCHASE2:%s",line.unit_price)
- Line.save([line])
+ #line unit_price calculation
+ if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
+ previous_linked_price = line.linked_price
+ line.sync_linked_price_from_basis()
+ unit_price = line.get_basis_price()
+ logger.info("VALIDATEPURCHASE:%s",unit_price)
+ if unit_price != line.unit_price or line.linked_price != previous_linked_price:
+ line.unit_price = unit_price
+ logger.info("VALIDATEPURCHASE2:%s",line.unit_price)
+ Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
@@ -1076,13 +1084,14 @@ class Line(metaclass=PoolMeta):
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
certif = fields.Many2One('purchase.certification',"Certification",states={'readonly': (Eval('inherit_cer')),})
inherit_cer = fields.Boolean("Inherit certification")
- enable_linked_currency = fields.Boolean("Linked currencies")
- linked_price = fields.Numeric("Price", digits='unit',states={
- 'invisible': (~Eval('enable_linked_currency')),
- })
- linked_currency = fields.Many2One('currency.linked',"Currency",states={
- 'invisible': (~Eval('enable_linked_currency')),
- })
+ enable_linked_currency = fields.Boolean("Linked currencies")
+ linked_price = fields.Numeric("Price", digits='unit',states={
+ 'invisible': (~Eval('enable_linked_currency')),
+ 'readonly': Eval('price_type') == 'basis',
+ }, depends=['enable_linked_currency', 'price_type'])
+ linked_currency = fields.Many2One('currency.linked',"Currency",states={
+ 'invisible': (~Eval('enable_linked_currency')),
+ })
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
@@ -1204,49 +1213,139 @@ class Line(metaclass=PoolMeta):
if ps:
return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)
- def getVirtualLot(self):
- if self.lots:
- return [l for l in self.lots if l.lot_type=='virtual'][0]
-
- def get_basis_price(self):
- price = Decimal(0)
- if self.terms:
- for t in self.terms:
- price += (t.manual_price if t.manual_price else Decimal(0))
- else:
+ def getVirtualLot(self):
+ if self.lots:
+ return [l for l in self.lots if l.lot_type=='virtual'][0]
+
+ def _get_linked_unit_factor(self):
+ if not (self.enable_linked_currency and self.linked_currency):
+ return None
+ factor = Decimal(self.linked_currency.factor or 0)
+ if not factor:
+ return None
+ unit_factor = Decimal(1)
+ if self.linked_unit:
+ Uom = Pool().get('product.uom')
+ unit_factor = Decimal(str(
+ Uom.compute_qty(self.unit, float(1), self.linked_unit) or 0))
+ return factor * unit_factor
+
+ def _linked_to_line_price(self, price):
+ factor = self._get_linked_unit_factor()
+ price = Decimal(price or 0)
+ if not factor:
+ return price
+ return round(price * factor, 4)
+
+ def _line_to_linked_price(self, price):
+ factor = self._get_linked_unit_factor()
+ price = Decimal(price or 0)
+ if not factor:
+ return price
+ return round(price / factor, 4)
+
+ def _get_premium_price(self):
+ premium = Decimal(self.premium or 0)
+ if not premium:
+ return Decimal(0)
+ if self.enable_linked_currency and self.linked_currency:
+ return self._linked_to_line_price(premium)
+ return premium
+
+ def _get_basis_component_price(self):
+ price = Decimal(0)
+ if self.terms:
+ for t in self.terms:
+ price += (t.manual_price if t.manual_price else Decimal(0))
+ else:
for pc in self.price_components:
PP = Pool().get('purchase.pricing.summary')
- pp = PP.search([('price_component','=',pc.id),('line','=',self.id)])
- if pp:
- price += pp[0].price * (pc.ratio / 100)
- return round(price,4)
+ pp = PP.search([('price_component','=',pc.id),('line','=',self.id)])
+ if pp:
+ price += pp[0].price * (pc.ratio / 100)
+ return round(price,4)
+
+ def get_basis_price(self):
+ return round(self._get_basis_component_price(), 4)
+
+ def sync_linked_price_from_basis(self):
+ if self.enable_linked_currency and self.linked_currency:
+ self.linked_price = self._line_to_linked_price(
+ self._get_basis_component_price())
+
+ def get_price(self,lot_premium=0):
+ return round(
+ Decimal(self.unit_price or 0)
+ + Decimal(lot_premium or 0),
+ 4)
+
+ def get_price_linked_currency(self,lot_premium=0):
+ return round(
+ self._linked_to_line_price(
+ Decimal(self.linked_price or 0)
+ + Decimal(lot_premium or 0)),
+ 4)
- def get_price(self,lot_premium=0):
- return (self.unit_price + Decimal(lot_premium)) if self.unit_price else Decimal(0) + (self.premium if self.premium else Decimal(0))
-
- def get_price_linked_currency(self,lot_premium=0):
- if self.linked_unit:
- Uom = Pool().get('product.uom')
- qt = Uom.compute_qty(self.unit, float(1), self.linked_unit)
- return round(((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor) * Decimal(qt), 4)
- else:
- return round((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor, 4)
-
- @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','premium','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
- def on_change_with_unit_price(self, name=None):
- Date = Pool().get('ir.date')
- logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
- if self.price_type == 'basis' and self.lots: #self.price_pricing and self.price_components and
- price = self.get_basis_price()
- logger.info("ONCHANGEUNITPRICE_IN:%s",price)
- return price
- if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
- return self.get_price_linked_currency()
- if self.price_type == 'efp':
- if hasattr(self, 'derivatives') and self.derivatives:
- for d in self.derivatives:
- return round(d.price_index.get_price(Date.today(),self.unit,self.purchase.currency,True),4)
- return self.unit_price
+ @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
+ def on_change_with_unit_price(self, name=None):
+ Date = Pool().get('ir.date')
+ logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
+ if self.price_type == 'basis':
+ self.sync_linked_price_from_basis()
+ price = self.get_basis_price()
+ logger.info("ONCHANGEUNITPRICE_IN:%s",price)
+ return price
+ if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
+ return self.get_price_linked_currency()
+ if self.price_type == 'efp':
+ if hasattr(self, 'derivatives') and self.derivatives:
+ for d in self.derivatives:
+ return round(d.price_index.get_price(Date.today(),self.unit,self.purchase.currency,True),4)
+ return self.get_price()
+
+ @fields.depends(
+ 'type', 'quantity', 'unit_price', 'purchase', '_parent_purchase.currency',
+ 'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
+ def on_change_with_amount(self):
+ if (self.type == 'line'
+ and self.quantity is not None
+ and self.unit_price is not None):
+ currency = self.purchase.currency if self.purchase else None
+ amount = Decimal(str(self.quantity)) * (
+ Decimal(self.unit_price or 0) + self._get_premium_price())
+ if currency:
+ return currency.round(amount)
+ return amount
+ return Decimal(0)
+
+ @fields.depends(methods=['on_change_with_unit_price', 'on_change_with_amount'])
+ def _recompute_trade_price_fields(self):
+ self.unit_price = self.on_change_with_unit_price()
+ self.amount = self.on_change_with_amount()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_premium(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_price_type(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_enable_linked_currency(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_linked_price(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_linked_currency(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_linked_unit(self):
+ self._recompute_trade_price_fields()
@classmethod
def write(cls, *args):
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index 82f9c8b..51cb4b2 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -394,16 +394,22 @@ class Sale(metaclass=PoolMeta):
return ''
@property
- def report_delivery(self):
- del_date = 'PROMPT'
- if self.lines:
- if self.lines[0].estimated_date:
- delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
- if delivery_date:
- del_date = delivery_date[0]
- if del_date:
- del_date = format_date_en(del_date)
- return del_date
+ def report_delivery(self):
+ del_date = 'PROMPT'
+ if self.lines:
+ if self.lines[0].estimated_date:
+ delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
+ if delivery_date:
+ del_date = delivery_date[0]
+ if del_date:
+ del_date = format_date_en(del_date)
+ return del_date
+
+ @property
+ def report_delivery_period_description(self):
+ if self.lines and self.lines[0].del_period:
+ return self.lines[0].del_period.description or ''
+ return ''
@property
def report_payment_date(self):
@@ -547,12 +553,14 @@ class Sale(metaclass=PoolMeta):
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_sale_line(line)
- if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
- unit_price = line.get_basis_price()
- if unit_price != line.unit_price:
- Line = Pool().get('sale.line')
- line.unit_price = unit_price
- Line.save([line])
+ if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
+ previous_linked_price = line.linked_price
+ line.sync_linked_price_from_basis()
+ unit_price = line.get_basis_price()
+ if unit_price != line.unit_price or line.linked_price != previous_linked_price:
+ Line = Pool().get('sale.line')
+ line.unit_price = unit_price
+ Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
@@ -621,13 +629,14 @@ class SaleLine(metaclass=PoolMeta):
# ('bci', 'BCI'),
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
inherit_cer = fields.Boolean("Inherit certification")
- enable_linked_currency = fields.Boolean("Linked currencies")
- linked_price = fields.Numeric("Price", digits='unit',states={
- 'invisible': (~Eval('enable_linked_currency')),
- })
- linked_currency = fields.Many2One('currency.linked',"Currency",states={
- 'invisible': (~Eval('enable_linked_currency')),
- })
+ enable_linked_currency = fields.Boolean("Linked currencies")
+ linked_price = fields.Numeric("Price", digits='unit',states={
+ 'invisible': (~Eval('enable_linked_currency')),
+ 'readonly': Eval('price_type') == 'basis',
+ }, depends=['enable_linked_currency', 'price_type'])
+ linked_currency = fields.Many2One('currency.linked',"Currency",states={
+ 'invisible': (~Eval('enable_linked_currency')),
+ })
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
@@ -757,44 +766,132 @@ class SaleLine(metaclass=PoolMeta):
if ps:
return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)
- def getVirtualLot(self):
- if self.lots:
- return [l for l in self.lots if l.lot_type=='virtual'][0]
-
- def get_price(self,lot_premium=0):
- return (self.unit_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) if self.unit_price else Decimal(0)
-
- def get_basis_price(self):
- price = Decimal(0)
- for pc in self.price_components:
- PP = Pool().get('sale.pricing.summary')
- pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
- if pp:
- price += pp[0].price * (pc.ratio / 100)
- return round(price,4)
-
- def get_price_linked_currency(self,lot_premium=0):
- if self.linked_unit:
- Uom = Pool().get('product.uom')
- qt = Uom.compute_qty(self.unit, float(1), self.linked_unit)
- return round(((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor) * Decimal(qt), 4)
- else:
- return round((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor, 4)
-
- @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','premium','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
- def on_change_with_unit_price(self, name=None):
- Date = Pool().get('ir.date')
- logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
- if self.price_type == 'basis' and self.lots: #self.price_pricing and self.price_components and
- logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
- return self.get_basis_price()
- if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
- return self.get_price_linked_currency()
- if self.price_type == 'efp':
- if hasattr(self, 'derivatives') and self.derivatives:
+ def getVirtualLot(self):
+ if self.lots:
+ return [l for l in self.lots if l.lot_type=='virtual'][0]
+
+ def _get_linked_unit_factor(self):
+ if not (self.enable_linked_currency and self.linked_currency):
+ return None
+ factor = Decimal(self.linked_currency.factor or 0)
+ if not factor:
+ return None
+ unit_factor = Decimal(1)
+ if self.linked_unit:
+ Uom = Pool().get('product.uom')
+ unit_factor = Decimal(str(
+ Uom.compute_qty(self.unit, float(1), self.linked_unit) or 0))
+ return factor * unit_factor
+
+ def _linked_to_line_price(self, price):
+ factor = self._get_linked_unit_factor()
+ price = Decimal(price or 0)
+ if not factor:
+ return price
+ return round(price * factor, 4)
+
+ def _line_to_linked_price(self, price):
+ factor = self._get_linked_unit_factor()
+ price = Decimal(price or 0)
+ if not factor:
+ return price
+ return round(price / factor, 4)
+
+ def _get_premium_price(self):
+ premium = Decimal(self.premium or 0)
+ if not premium:
+ return Decimal(0)
+ if self.enable_linked_currency and self.linked_currency:
+ return self._linked_to_line_price(premium)
+ return premium
+
+ def get_price(self,lot_premium=0):
+ return round(
+ Decimal(self.unit_price or 0)
+ + Decimal(lot_premium or 0),
+ 4)
+
+ def _get_basis_component_price(self):
+ price = Decimal(0)
+ for pc in self.price_components:
+ PP = Pool().get('sale.pricing.summary')
+ pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
+ if pp:
+ price += pp[0].price * (pc.ratio / 100)
+ return round(price,4)
+
+ def get_basis_price(self):
+ return round(self._get_basis_component_price(), 4)
+
+ def sync_linked_price_from_basis(self):
+ if self.enable_linked_currency and self.linked_currency:
+ self.linked_price = self._line_to_linked_price(
+ self._get_basis_component_price())
+
+ def get_price_linked_currency(self,lot_premium=0):
+ return round(
+ self._linked_to_line_price(
+ Decimal(self.linked_price or 0)
+ + Decimal(lot_premium or 0)),
+ 4)
+
+ @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
+ def on_change_with_unit_price(self, name=None):
+ Date = Pool().get('ir.date')
+ logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
+ if self.price_type == 'basis':
+ self.sync_linked_price_from_basis()
+ logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
+ return self.get_basis_price()
+ if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
+ return self.get_price_linked_currency()
+ if self.price_type == 'efp':
+ if hasattr(self, 'derivatives') and self.derivatives:
for d in self.derivatives:
return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True)
- return self.get_price()
+ return self.get_price()
+
+ @fields.depends(
+ 'type', 'quantity', 'unit_price', 'sale', '_parent_sale.currency',
+ 'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
+ def on_change_with_amount(self):
+ if self.type == 'line':
+ currency = self.sale.currency if self.sale else None
+ amount = Decimal(str(self.quantity or 0)) * (
+ Decimal(self.unit_price or 0) + self._get_premium_price())
+ if currency:
+ return currency.round(amount)
+ return amount
+ return Decimal(0)
+
+ @fields.depends(methods=['on_change_with_unit_price', 'on_change_with_amount'])
+ def _recompute_trade_price_fields(self):
+ self.unit_price = self.on_change_with_unit_price()
+ self.amount = self.on_change_with_amount()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_premium(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_price_type(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_enable_linked_currency(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_linked_price(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_linked_currency(self):
+ self._recompute_trade_price_fields()
+
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_linked_unit(self):
+ self._recompute_trade_price_fields()
def check_from_to(self,tr):
if tr.pricing_period:
diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py
index 4271ea7..bf71f45 100644
--- a/modules/purchase_trade/valuation.py
+++ b/modules/purchase_trade/valuation.py
@@ -188,9 +188,22 @@ class ValuationBase(ModelSQL):
@staticmethod
def _supports_strategy_mtm(values):
return values and values.get('type') in {'pur. priced', 'sale priced'}
+
+ @staticmethod
+ def _get_basis_component_total(record):
+ getter = getattr(record, '_get_basis_component_price', None)
+ if getter:
+ return Decimal(getter() or 0)
+ return Decimal(0)
+
+ @classmethod
+ def _get_basis_premium_delta(cls, record):
+ total = Decimal(record.get_basis_price() or 0)
+ components = cls._get_basis_component_total(record)
+ return round(total - components, 4)
@classmethod
- def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign):
+ def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign, extra_price=Decimal(0)):
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
values = cls._base_pnl(
@@ -210,6 +223,7 @@ class ValuationBase(ModelSQL):
logger.info("COMPONENTS:%s",c)
if c:
price = c[0].manual_price
+ price = Decimal(price or 0) + Decimal(extra_price or 0)
values.update({
'reference': f"{pc.get_name()} / {pc.ratio}%",
@@ -305,8 +319,11 @@ class ValuationBase(ModelSQL):
for lot in line.lots:
if line.price_type == 'basis':
+ premium_delta = cls._get_basis_premium_delta(line)
for pc in line.price_summary or []:
- values = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1)
+ values = cls._build_basis_pnl(
+ line=line, lot=lot, sale_line=None, pc=pc, sign=-1,
+ extra_price=premium_delta)
if values and lot.sale_line:
values['sale'] = lot.sale_line.sale.id
values['sale_line'] = lot.sale_line.id
@@ -361,8 +378,11 @@ class ValuationBase(ModelSQL):
continue
if sl_line.price_type == 'basis':
+ premium_delta = cls._get_basis_premium_delta(sl_line)
for pc in sl_line.price_summary or []:
- values = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1)
+ values = cls._build_basis_pnl(
+ line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1,
+ extra_price=premium_delta)
if sl_line.mtm and cls._supports_strategy_mtm(values):
for strat in line.mtm:
values['mtm_price'] = cls._get_strategy_mtm_price(strat, sl_line)
@@ -400,7 +420,7 @@ class ValuationBase(ModelSQL):
return price_lines
@classmethod
- def _build_basis_pnl_from_sale_line(cls, *, sale_line, lot, pc):
+ def _build_basis_pnl_from_sale_line(cls, *, sale_line, lot, pc, extra_price=Decimal(0)):
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
values = cls._base_sale_pnl(
@@ -410,7 +430,7 @@ class ValuationBase(ModelSQL):
)
qty = lot.get_current_quantity_converted()
- price = pc.price
+ price = Decimal(pc.price or 0) + Decimal(extra_price or 0)
values.update({
'reference': f"{pc.get_name()} / {pc.ratio}%",
@@ -506,7 +526,7 @@ class ValuationBase(ModelSQL):
values = cls._build_simple_pnl_from_sale_line(
sale_line=sale_line,
lot=lot,
- price=Decimal(0),
+ price=Decimal(sale_line.unit_price or 0),
state='unfixed',
pnl_type='sale priced'
)
@@ -523,9 +543,11 @@ class ValuationBase(ModelSQL):
price_lines.append(values)
continue
+ premium_delta = cls._get_basis_premium_delta(sale_line)
for pc in summaries:
values = cls._build_basis_pnl_from_sale_line(
- sale_line=sale_line, lot=lot, pc=pc)
+ sale_line=sale_line, lot=lot, pc=pc,
+ extra_price=premium_delta)
if sale_line.mtm and cls._supports_strategy_mtm(values):
for strat in sale_line.mtm:
values['mtm_price'] = cls._get_strategy_mtm_price(strat, sale_line)
diff --git a/modules/purchase_trade/view/purchase_line_tree_sequence.xml b/modules/purchase_trade/view/purchase_line_tree_sequence.xml
index 18a7c67..51b3e08 100755
--- a/modules/purchase_trade/view/purchase_line_tree_sequence.xml
+++ b/modules/purchase_trade/view/purchase_line_tree_sequence.xml
@@ -3,8 +3,9 @@
this repository contains the full copyright notices and license terms. -->
+
-
\ No newline at end of file
+
diff --git a/modules/purchase_trade/view/sale_line_tree_sequence.xml b/modules/purchase_trade/view/sale_line_tree_sequence.xml
index 18a7c67..51b3e08 100755
--- a/modules/purchase_trade/view/sale_line_tree_sequence.xml
+++ b/modules/purchase_trade/view/sale_line_tree_sequence.xml
@@ -3,8 +3,9 @@
this repository contains the full copyright notices and license terms. -->
+
-
\ No newline at end of file
+
diff --git a/modules/sale/sale_ict.fodt b/modules/sale/sale_ict.fodt
index d944063..1627c8c 100644
--- a/modules/sale/sale_ict.fodt
+++ b/modules/sale/sale_ict.fodt
@@ -4202,7 +4202,7 @@
- <sale.lines[0].del_period.description if sale.lines[0].del_period else ''>
+ <sale.report_delivery_period_description>
@@ -4389,4 +4389,4 @@
-
\ No newline at end of file
+