01.04.26
This commit is contained in:
@@ -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`)
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='product']" position="after">
|
||||
<field name="premium"/>
|
||||
<field name="del_period"/>
|
||||
<field name="from_del"/>
|
||||
<field name="to_del"/>
|
||||
</xpath>
|
||||
</data>
|
||||
</data>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='product']" position="after">
|
||||
<field name="premium"/>
|
||||
<field name="del_period"/>
|
||||
<field name="from_del"/>
|
||||
<field name="to_del"/>
|
||||
</xpath>
|
||||
</data>
|
||||
</data>
|
||||
|
||||
@@ -4202,7 +4202,7 @@
|
||||
<text:p text:style-name="P36">
|
||||
<text:s />
|
||||
<text:span text:style-name="T24">
|
||||
<text:placeholder text:placeholder-type="text"><sale.lines[0].del_period.description if sale.lines[0].del_period else ''></text:placeholder>
|
||||
<text:placeholder text:placeholder-type="text"><sale.report_delivery_period_description></text:placeholder>
|
||||
</text:span>
|
||||
</text:p>
|
||||
<text:p text:style-name="P34" />
|
||||
@@ -4389,4 +4389,4 @@
|
||||
</text:p>
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document>
|
||||
</office:document>
|
||||
|
||||
Reference in New Issue
Block a user