diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index 118eab8..515cfa3 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -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,
diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py
index 02c74f6..62fd50e 100755
--- a/modules/purchase_trade/fee.py
+++ b/modules/purchase_trade/fee.py
@@ -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)
diff --git a/modules/purchase_trade/fee.xml b/modules/purchase_trade/fee.xml
index dca08ce..424b39f 100755
--- a/modules/purchase_trade/fee.xml
+++ b/modules/purchase_trade/fee.xml
@@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. -->
fee_tree_sequence
-
- valuation.valuation
- tree
- valuation_tree_sequence3
-
-
- valuation.valuation
- graph
- valuation_graph
-
-
- valuation.valuation
- graph
- valuation_graph2
-
-
- valuation.valuation.dyn
- tree
- valuation_tree_sequence4
-
fee.fee
tree
diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py
index b81e2cc..24f529d 100755
--- a/modules/purchase_trade/purchase.py
+++ b/modules/purchase_trade/purchase.py
@@ -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')
diff --git a/modules/purchase_trade/tryton.cfg b/modules/purchase_trade/tryton.cfg
index 0ff0286..015d586 100755
--- a/modules/purchase_trade/tryton.cfg
+++ b/modules/purchase_trade/tryton.cfg
@@ -28,4 +28,5 @@ xml:
party.xml
forex.xml
global_reporting.xml
- derivative.xml
\ No newline at end of file
+ derivative.xml
+ valuation.xml
\ No newline at end of file
diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py
new file mode 100644
index 0000000..8f3f00c
--- /dev/null
+++ b/modules/purchase_trade/valuation.py
@@ -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
diff --git a/modules/purchase_trade/valuation.xml b/modules/purchase_trade/valuation.xml
new file mode 100644
index 0000000..63ba7dc
--- /dev/null
+++ b/modules/purchase_trade/valuation.xml
@@ -0,0 +1,24 @@
+
+
+
+ valuation.valuation
+ tree
+ valuation_tree_sequence3
+
+
+ valuation.valuation
+ graph
+ valuation_graph
+
+
+ valuation.valuation
+ graph
+ valuation_graph2
+
+
+ valuation.valuation.dyn
+ tree
+ valuation_tree_sequence4
+
+
+
\ No newline at end of file