This commit is contained in:
2026-01-04 13:31:17 +01:00
parent 37d73b6962
commit 02b99b4622
7 changed files with 438 additions and 144 deletions

View File

@@ -3,7 +3,32 @@
from trytond.pool import Pool
from . import purchase,sale,global_reporting,stock,derivative,lot,pricing,workflow,lc,dashboard,fee,payment_term,purchase_prepayment,cron,party,forex,outgoing,incoming,optional,association_tables, document_tracking, open_position, credit_risk
from . import (
purchase,
sale,
global_reporting,
stock,
derivative,
lot,
pricing,
workflow,
lc,
dashboard,
fee,
payment_term,
purchase_prepayment,
cron,
party,
forex,
outgoing,
incoming,
optional,
association_tables,
document_tracking,
open_position,
credit_risk,
valuation,
)
def register():
Pool.register(
@@ -69,8 +94,8 @@ def register():
fee.Fee,
fee.FeeLots,
purchase.FeeLots,
fee.Valuation,
fee.ValuationDyn,
valuation.Valuation,
valuation.ValuationDyn,
derivative.Derivative,
derivative.DerivativeMatch,
derivative.MatchWizardStart,

View File

@@ -21,117 +21,6 @@ from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
VALTYPE = [
('priced', 'Price'),
('pur. priced', 'Pur. price'),
('pur. efp', 'Pur. efp'),
('sale priced', 'Sale price'),
('sale efp', 'Sale efp'),
('line fee', 'Line fee'),
('pur. fee', 'Pur. fee'),
('sale fee', 'Sale fee'),
('shipment fee', 'Shipment fee'),
('market', 'Market'),
('derivative', 'Derivative'),
]
class Valuation(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation'
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Purch. Line")
date = fields.Date("Date")
type = fields.Selection(VALTYPE, "Type")
reference = fields.Char("Reference")
counterparty = fields.Many2One('party.party',"Counterparty")
product = fields.Many2One('product.product',"Product")
state = fields.Char("State")
price = fields.Numeric("Price",digits='unit')
currency = fields.Many2One('currency.currency',"Cur")
quantity = fields.Numeric("Quantity",digits='unit')
unit = fields.Many2One('product.uom',"Unit")
amount = fields.Numeric("Amount",digits='unit')
mtm = fields.Numeric("Mtm",digits='unit')
lot = fields.Many2One('lot.lot',"Lot")
class ValuationDyn(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation.dyn'
r_purchase = fields.Many2One('purchase.purchase',"Purchase")
r_line = fields.Many2One('purchase.line',"Line")
r_date = fields.Date("Date")
r_type = fields.Selection(VALTYPE, "Type")
r_reference = fields.Char("Reference")
r_counterparty = fields.Many2One('party.party',"Counterparty")
r_product = fields.Many2One('product.product',"Product")
r_state = fields.Char("State")
r_price = fields.Numeric("Price",digits='r_unit')
r_currency = fields.Many2One('currency.currency',"Cur")
r_quantity = fields.Numeric("Quantity",digits='r_unit')
r_unit = fields.Many2One('product.uom',"Unit")
r_amount = fields.Numeric("Amount",digits='r_unit')
r_mtm = fields.Numeric("Mtm",digits='r_unit')
r_lot = fields.Many2One('lot.lot',"Lot")
@classmethod
def table_query(cls):
Valuation = Pool().get('valuation.valuation')
val = Valuation.__table__()
context = Transaction().context
group_pnl = context.get('group_pnl')
wh = (val.id > 0)
# query = val.select(
# Literal(0).as_('create_uid'),
# CurrentTimestamp().as_('create_date'),
# Literal(None).as_('write_uid'),
# Literal(None).as_('write_date'),
# val.id.as_('id'),
# val.purchase.as_('r_purchase'),
# val.line.as_('r_line'),
# val.date.as_('r_date'),
# val.type.as_('r_type'),
# val.reference.as_('r_reference'),
# val.counterparty.as_('r_counterparty'),
# val.product.as_('r_product'),
# val.state.as_('r_state'),
# val.price.as_('r_price'),
# val.currency.as_('r_currency'),
# val.quantity.as_('r_quantity'),
# val.unit.as_('r_unit'),
# val.amount.as_('r_amount'),
# val.mtm.as_('r_mtm'),
# val.lot.as_('r_lot'),
# where=wh)
#if group_pnl==True:
query = val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(val.id).as_('id'),
Max(val.purchase).as_('r_purchase'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.mtm).as_('r_mtm'),
Max(val.lot).as_('r_lot'),
where=wh,
group_by=[val.type,val.counterparty,val.state])
return query
def filter_state(state):
def filter(func):
@wraps(func)

View File

@@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">fee_tree_sequence</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
<field name="model">valuation.valuation</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence3</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph2">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph2</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence4">
<field name="model">valuation.valuation.dyn</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence4</field>
</record>
<record model="ir.ui.view" id="fee_view_tree_sequence2">
<field name="model">fee.fee</field>
<field name="type">tree</field>

View File

@@ -380,19 +380,11 @@ class Purchase(metaclass=PoolMeta):
Decimal(str(line.quantity))
.quantize(Decimal("0.00001"))
)
Line.save([line])
#compute pnl
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',line.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(line.get_pnl_fee_lines())
pnl_lines.extend(line.get_pnl_price_lines())
pnl_lines.extend(line.get_pnl_der_lines())
Pnl.save(pnl_lines)
Pnl.generate(line)
if line.quantity_theorical:
OpenPosition = Pool().get('open.position')

View File

@@ -28,4 +28,5 @@ xml:
party.xml
forex.xml
global_reporting.xml
derivative.xml
derivative.xml
valuation.xml

View File

@@ -0,0 +1,383 @@
from trytond.model import fields
from trytond.report import Report
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
VALTYPE = [
('priced', 'Price'),
('pur. priced', 'Pur. price'),
('pur. efp', 'Pur. efp'),
('sale priced', 'Sale price'),
('sale efp', 'Sale efp'),
('line fee', 'Line fee'),
('pur. fee', 'Pur. fee'),
('sale fee', 'Sale fee'),
('shipment fee', 'Shipment fee'),
('market', 'Market'),
('derivative', 'Derivative'),
]
class Valuation(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation'
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Purch. Line")
date = fields.Date("Date")
type = fields.Selection(VALTYPE, "Type")
reference = fields.Char("Reference")
counterparty = fields.Many2One('party.party',"Counterparty")
product = fields.Many2One('product.product',"Product")
state = fields.Char("State")
price = fields.Numeric("Price",digits='unit')
currency = fields.Many2One('currency.currency',"Cur")
quantity = fields.Numeric("Quantity",digits='unit')
unit = fields.Many2One('product.uom',"Unit")
amount = fields.Numeric("Amount",digits='unit')
mtm = fields.Numeric("Mtm",digits='unit')
lot = fields.Many2One('lot.lot',"Lot")
@classmethod
def _base_pnl(cls, *, line, lot, pnl_type, sale=None):
Date = Pool().get('ir.date')
pnl = Pool().get('valuation.valuation')()
pnl.purchase = line.purchase.id
pnl.line = line.id
pnl.type = pnl_type
pnl.date = Date.today()
pnl.lot = lot.id
if sale:
pnl.sale = sale.id
return pnl
@classmethod
def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign):
pnl = cls._base_pnl(
line=line,
lot=lot,
sale=sale_line.sale if sale_line else None,
pnl_type='sale priced' if sale_line else 'pur. priced'
)
qty = lot.get_current_quantity_converted()
pnl.reference = f"{pc.get_name()} / {pc.ratio}%"
pnl.price = round(pc.price, 4)
pnl.counterparty = sale_line.sale.party if sale_line else line.purchase.party
pnl.product = sale_line.product if sale_line else line.product
# State
if pc.unfixed_qt == 0:
pnl.state = 'fixed'
elif pc.fixed_qt == 0:
pnl.state = 'unfixed'
else:
base = sale_line.quantity_theorical if sale_line else line.quantity_theorical
pnl.state = f"part. fixed {round(pc.fixed_qt / Decimal(base) * 100, 0)}%"
if pc.price and pc.ratio:
pnl.quantity = round(qty, 5)
pnl.amount = round(pc.price * qty * Decimal(sign) * pc.ratio / 100, 4)
mtm = Decimal(0)
last_price = pc.get_last_price()
if last_price:
mtm = round(Decimal(last_price) * qty * Decimal(sign), 4)
pnl.mtm = round(pnl.amount - (mtm * pc.ratio / 100), 4)
pnl.unit = sale_line.unit if sale_line else line.unit
pnl.currency = sale_line.sale.currency if sale_line else line.purchase.currency
return pnl
@classmethod
def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign, pnl_type):
pnl = cls._base_pnl(
line=line,
lot=lot,
sale=sale_line.sale if sale_line else None,
pnl_type=pnl_type
)
qty = lot.get_current_quantity_converted()
pnl.price = round(price, 4)
pnl.quantity = round(qty, 5)
pnl.amount = round(pnl.price * qty * Decimal(sign), 4)
pnl.mtm = Decimal(0)
pnl.state = state
pnl.unit = sale_line.unit if sale_line else line.unit
pnl.currency = sale_line.sale.currency if sale_line else line.purchase.currency
pnl.counterparty = sale_line.sale.party if sale_line else line.purchase.party
pnl.product = sale_line.product if sale_line else line.product
pnl.reference = 'Sale/Physic' if lot.lot_type == 'physic' else 'Sale/Open' if sale_line else 'Purchase/Physic'
return pnl
@classmethod
def create_pnl_price_from_line(cls, line):
price_lines = []
LotQt = Pool().get('lot.qt')
for lot in line.lots:
# --- PURCHASE SIDE ---
if line.price_type == 'basis':
for pc in line.price_summary or []:
pnl = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1)
if pnl:
price_lines.append(pnl)
elif line.price_type in ('priced', 'efp') and lot.lot_price:
state = 'fixed' if line.price_type == 'priced' else 'not fixed'
price_lines.append(
cls._build_simple_pnl(
line=line,
lot=lot,
sale_line=None,
price=lot.lot_price,
state=state,
sign=-1,
pnl_type=f'pur. {line.price_type}'
)
)
# --- SALE SIDE ---
sale_lots = [lot] if lot.sale_line else [
lqt.lot_s for lqt in LotQt.search([('lot_p','=',lot.id),('lot_s','>',0),('lot_quantity','>',0)])
]
for sl in sale_lots:
sl_line = sl.sale_line
if not sl_line:
continue
if sl_line.price_type == 'basis':
for pc in sl_line.price_summary or []:
pnl = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1)
if pnl:
price_lines.append(pnl)
elif sl_line.price_type in ('priced', 'efp'):
state = 'fixed' if sl_line.price_type == 'priced' else 'not fixed'
price = sl.lot_price_sale
price_lines.append(
cls._build_simple_pnl(
line=line,
lot=sl,
sale_line=sl_line,
price=price,
state=state,
sign=+1,
pnl_type=f'sale {sl_line.price_type}'
)
)
return price_lines
@classmethod
def group_fees_by_type_supplier(cls,line,fees):
grouped = defaultdict(list)
# Regrouper par (type, supplier)
for fee in fees:
key = (fee.product, fee.supplier)
grouped[key].append(fee)
result = []
for key, fee_list in grouped.items():
ordered_fees = [f for f in fee_list if f.type == 'ordered']
if ordered_fees:
result.extend(ordered_fees)
else:
budgeted_fees = [f for f in fee_list if f.type == 'budgeted']
result.extend(budgeted_fees)
return result
@classmethod
def create_pnl_fee_from_line(cls,line):
fee_lines = []
#pnl management
Pnl = Pool().get('valuation.valuation')
Date = Pool().get('ir.date')
Currency = Pool().get('currency.currency')
FeeLots = Pool().get('fee.lots')
if line.lots:
for lot in line.lots:
fl = FeeLots.search(['lot','=',lot.id])
if fl:
fees = [e.fee for e in fl]
sorted_fees = cls.group_fees_by_type_supplier(line,fees)
if sorted_fees:
for sf in sorted_fees:
pnl = Pnl()
pnl.lot = lot.id
if lot.sale_line:
pnl.sale = lot.sale_line.sale.id
pnl.purchase = line.purchase.id
pnl.line = line.id
if sf.line:
pnl.type = 'pur. fee'
if sf.sale_line:
pnl.type = 'sale fee'
if sf.shipment_in:
pnl.type = 'shipment fee'
pnl.date = Date.today()
pnl.price = Decimal(sf.get_price_per_qt())
if sf.currency != line.purchase.currency:
with Transaction().set_context(date=Date.today()):
pnl.price = Currency.compute(sf.currency,pnl.price, line.purchase.currency)
pnl.counterparty = sf.supplier
str_op = ''
if lot.lot_type == 'physic':
str_op = '/Physic'
else:
str_op = '/Open'
pnl.reference = sf.product.name + str_op
pnl.product = sf.product
pnl.state = sf.type
if sf.p_r == 'pay':
sign = -1
pnl.amount = round(pnl.price * lot.get_current_quantity_converted() * sign,2)
pnl.mtm = 0
pnl.quantity = round(lot.get_current_quantity_converted(),5)
pnl.unit = sf.unit if sf.unit else line.unit
pnl.currency = sf.currency
fee_lines.append(pnl)
return fee_lines
@classmethod
def create_pnl_der_from_line(cls,line):
der_lines = []
if line.derivatives:
Pnl = Pool().get('valuation.valuation')
Date = Pool().get('ir.date')
for d in line.derivatives:
pnl = Pnl()
pnl.purchase = line.purchase.id
pnl.line = line.id
pnl.type = 'derivative'
pnl.date = Date.today()
pnl.reference = d.price_index.price_index
pnl.price = round(Decimal(d.price_index.get_price_per_qt(d.price,line.unit,line.purchase.currency)),4)
pnl.counterparty = d.party
pnl.product = d.product
pnl.state = 'fixed'
pnl.amount = round(pnl.price * (d.quantity) * Decimal(-1),4)
mtm = round(Decimal(d.price_index.get_price(Date.today(),line.unit,line.purchase.currency,True)) * d.quantity * Decimal(-1),4)
pnl.mtm = pnl.amount - mtm
pnl.quantity = round(d.quantity,5)
pnl.unit = line.unit
pnl.currency = line.purchase.currency
der_lines.append(pnl)
return der_lines
@classmethod
def generate(cls, line):
Date = Pool().get('ir.date')
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',line.id),('date','=',Date.today())])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(cls.create_pnl_fee_from_line(line))
pnl_lines.extend(cls.create_pnl_price_from_line(line))
pnl_lines.extend(cls.create_pnl_der_from_line(line))
Pnl.save(pnl_lines)
class ValuationDyn(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation.dyn'
r_purchase = fields.Many2One('purchase.purchase',"Purchase")
r_line = fields.Many2One('purchase.line',"Line")
r_date = fields.Date("Date")
r_type = fields.Selection(VALTYPE, "Type")
r_reference = fields.Char("Reference")
r_counterparty = fields.Many2One('party.party',"Counterparty")
r_product = fields.Many2One('product.product',"Product")
r_state = fields.Char("State")
r_price = fields.Numeric("Price",digits='r_unit')
r_currency = fields.Many2One('currency.currency',"Cur")
r_quantity = fields.Numeric("Quantity",digits='r_unit')
r_unit = fields.Many2One('product.uom',"Unit")
r_amount = fields.Numeric("Amount",digits='r_unit')
r_mtm = fields.Numeric("Mtm",digits='r_unit')
r_lot = fields.Many2One('lot.lot',"Lot")
@classmethod
def table_query(cls):
Valuation = Pool().get('valuation.valuation')
val = Valuation.__table__()
context = Transaction().context
group_pnl = context.get('group_pnl')
wh = (val.id > 0)
# query = val.select(
# Literal(0).as_('create_uid'),
# CurrentTimestamp().as_('create_date'),
# Literal(None).as_('write_uid'),
# Literal(None).as_('write_date'),
# val.id.as_('id'),
# val.purchase.as_('r_purchase'),
# val.line.as_('r_line'),
# val.date.as_('r_date'),
# val.type.as_('r_type'),
# val.reference.as_('r_reference'),
# val.counterparty.as_('r_counterparty'),
# val.product.as_('r_product'),
# val.state.as_('r_state'),
# val.price.as_('r_price'),
# val.currency.as_('r_currency'),
# val.quantity.as_('r_quantity'),
# val.unit.as_('r_unit'),
# val.amount.as_('r_amount'),
# val.mtm.as_('r_mtm'),
# val.lot.as_('r_lot'),
# where=wh)
#if group_pnl==True:
query = val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(val.id).as_('id'),
Max(val.purchase).as_('r_purchase'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.mtm).as_('r_mtm'),
Max(val.lot).as_('r_lot'),
where=wh,
group_by=[val.type,val.counterparty,val.state])
return query

View File

@@ -0,0 +1,24 @@
<tryton>
<data>
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
<field name="model">valuation.valuation</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence3</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph2">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph2</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence4">
<field name="model">valuation.valuation.dyn</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence4</field>
</record>
</data>
</tryton>