This commit is contained in:
2026-05-24 18:19:57 +02:00
parent 1075f49bd1
commit f0ed5b9bd3
8 changed files with 317 additions and 0 deletions

View File

@@ -68,6 +68,10 @@ def register():
ctrm_reporting.CTRMRealizedPnlContext, ctrm_reporting.CTRMRealizedPnlContext,
ctrm_reporting.CTRMMtmPnl, ctrm_reporting.CTRMMtmPnl,
ctrm_reporting.CTRMMtmPnlContext, ctrm_reporting.CTRMMtmPnlContext,
ctrm_reporting.CTRMPnlExplain,
ctrm_reporting.CTRMPnlExplainContext,
ctrm_reporting.CTRMPnlDimension,
ctrm_reporting.CTRMPnlDimensionContext,
configuration.Configuration, configuration.Configuration,
pricing.ImportPricesStart, pricing.ImportPricesStart,
pricing.ImportPricesResult, pricing.ImportPricesResult,

View File

@@ -555,3 +555,198 @@ class CTRMMtmPnl(ModelSQL, ModelView):
'mtm_delta'), 'mtm_delta'),
val.strategy.as_('strategy'), val.strategy.as_('strategy'),
where=where) where=where)
class CTRMPnlExplainContext(CTRMPnlContextMixin, ModelView):
"CTRM P&L Explain Context"
__name__ = 'ctrm.reporting.pnl.explain.context'
class CTRMPnlExplain(ModelSQL, ModelView):
"CTRM P&L Explain"
__name__ = 'ctrm.reporting.pnl.explain'
valuation_date = fields.Date("Valuation Date")
product = fields.Many2One('product.product', "Product")
counterparty = fields.Many2One('party.party', "Counterparty")
currency = fields.Many2One('currency.currency', "Currency")
unit = fields.Many2One('product.uom', "Unit")
state = fields.Char("State")
strategy = fields.Many2One('mtm.strategy', "Strategy")
physical_pnl = fields.Numeric("Physical P&L", digits=(16, 2))
fee_pnl = fields.Numeric("Fee P&L", digits=(16, 2))
derivative_pnl = fields.Numeric("Derivative P&L", digits=(16, 2))
mtm_delta = fields.Numeric("MTM Delta", digits=(16, 2))
total_pnl = fields.Numeric("Total P&L", digits=(16, 2))
@classmethod
def table_query(cls):
ValuationLine = Pool().get('valuation.valuation.line')
val = ValuationLine.__table__()
context = Transaction().context
where = val.type.in_(ALL_VALUATION_TYPES)
if context.get('date'):
where &= val.date == context['date']
if context.get('product'):
where &= val.product == context['product']
if context.get('counterparty'):
where &= val.counterparty == context['counterparty']
if context.get('currency'):
where &= val.currency == context['currency']
if context.get('purchase'):
where &= val.purchase == context['purchase']
if context.get('sale'):
where &= val.sale == context['sale']
if context.get('strategy'):
where &= val.strategy == context['strategy']
if context.get('state'):
where &= val.state == context['state']
is_fee = val.type.in_([
'line fee',
'pur. fee',
'sale fee',
'shipment fee',
])
is_derivative = val.type.in_(DERIVATIVE_VALUATION_TYPES)
physical_pnl = Case(
(is_fee | is_derivative, 0), else_=Coalesce(val.amount, 0))
fee_pnl = Case((is_fee, Coalesce(val.amount, 0)), else_=0)
derivative_pnl = Case(
(is_derivative, Coalesce(val.amount, 0)), else_=0)
mtm_delta = Coalesce(val.mtm, 0) - Coalesce(val.amount, 0)
group_by = [
val.date,
val.product,
val.counterparty,
val.currency,
val.unit,
val.state,
val.strategy,
]
return val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Min(val.id).as_('id'),
val.date.as_('valuation_date'),
val.product.as_('product'),
val.counterparty.as_('counterparty'),
val.currency.as_('currency'),
val.unit.as_('unit'),
val.state.as_('state'),
val.strategy.as_('strategy'),
Sum(physical_pnl).as_('physical_pnl'),
Sum(fee_pnl).as_('fee_pnl'),
Sum(derivative_pnl).as_('derivative_pnl'),
Sum(mtm_delta).as_('mtm_delta'),
Sum(Coalesce(val.amount, 0)).as_('total_pnl'),
where=where,
group_by=group_by)
class CTRMPnlDimensionContext(CTRMPnlContextMixin, ModelView):
"CTRM P&L by Dimension Context"
__name__ = 'ctrm.reporting.pnl.dimension.context'
dimension = fields.Many2One('analytic.dimension', "Dimension")
value = fields.Many2One('analytic.dimension.value', "Value")
class CTRMPnlDimension(ModelSQL, ModelView):
"CTRM P&L by Dimension"
__name__ = 'ctrm.reporting.pnl.dimension'
valuation_date = fields.Date("Valuation Date")
dimension = fields.Many2One('analytic.dimension', "Dimension")
value = fields.Many2One('analytic.dimension.value', "Value")
product = fields.Many2One('product.product', "Product")
counterparty = fields.Many2One('party.party', "Counterparty")
currency = fields.Many2One('currency.currency', "Currency")
unit = fields.Many2One('product.uom', "Unit")
state = fields.Char("State")
strategy = fields.Many2One('mtm.strategy', "Strategy")
quantity = fields.Numeric("Quantity", digits=(16, 5))
amount = fields.Numeric("Amount", digits=(16, 2))
mtm = fields.Numeric("MTM", digits=(16, 2))
mtm_delta = fields.Numeric("MTM Delta", digits=(16, 2))
@classmethod
def table_query(cls):
ValuationLine = Pool().get('valuation.valuation.line')
Assignment = Pool().get('analytic.dimension.assignment')
val = ValuationLine.__table__()
purchase_assignment = Assignment.__table__()
sale_assignment = Assignment.__table__()
context = Transaction().context
dimension_expr = Coalesce(
sale_assignment.dimension, purchase_assignment.dimension)
value_expr = Coalesce(sale_assignment.value, purchase_assignment.value)
where = val.type.in_(ALL_VALUATION_TYPES)
if context.get('date'):
where &= val.date == context['date']
if context.get('product'):
where &= val.product == context['product']
if context.get('counterparty'):
where &= val.counterparty == context['counterparty']
if context.get('currency'):
where &= val.currency == context['currency']
if context.get('purchase'):
where &= val.purchase == context['purchase']
if context.get('sale'):
where &= val.sale == context['sale']
if context.get('strategy'):
where &= val.strategy == context['strategy']
if context.get('state'):
where &= val.state == context['state']
if context.get('dimension'):
where &= dimension_expr == context['dimension']
if context.get('value'):
where &= value_expr == context['value']
group_by = [
val.date,
dimension_expr,
value_expr,
val.product,
val.counterparty,
val.currency,
val.unit,
val.state,
val.strategy,
]
return (
val
.join(purchase_assignment, 'LEFT',
condition=purchase_assignment.purchase == val.purchase)
.join(sale_assignment, 'LEFT',
condition=sale_assignment.sale == val.sale)
.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Min(val.id).as_('id'),
val.date.as_('valuation_date'),
dimension_expr.as_('dimension'),
value_expr.as_('value'),
val.product.as_('product'),
val.counterparty.as_('counterparty'),
val.currency.as_('currency'),
val.unit.as_('unit'),
val.state.as_('state'),
val.strategy.as_('strategy'),
Sum(Coalesce(val.quantity, 0)).as_('quantity'),
Sum(Coalesce(val.amount, 0)).as_('amount'),
Sum(Coalesce(val.mtm, 0)).as_('mtm'),
Sum(Coalesce(val.mtm, 0) - Coalesce(val.amount, 0)).as_(
'mtm_delta'),
where=where,
group_by=group_by))

View File

@@ -100,6 +100,46 @@
<field name="view" ref="ctrm_pnl_mtm_view_list"/> <field name="view" ref="ctrm_pnl_mtm_view_list"/>
<field name="act_window" ref="act_ctrm_pnl_mtm"/> <field name="act_window" ref="act_ctrm_pnl_mtm"/>
</record> </record>
<record model="ir.ui.view" id="ctrm_pnl_explain_context_view_form">
<field name="model">ctrm.reporting.pnl.explain.context</field>
<field name="type">form</field>
<field name="name">ctrm_pnl_explain_context_form</field>
</record>
<record model="ir.ui.view" id="ctrm_pnl_explain_view_list">
<field name="model">ctrm.reporting.pnl.explain</field>
<field name="type">tree</field>
<field name="name">ctrm_pnl_explain_list</field>
</record>
<record model="ir.action.act_window" id="act_ctrm_pnl_explain">
<field name="name">P&amp;L Explain</field>
<field name="res_model">ctrm.reporting.pnl.explain</field>
<field name="context_model">ctrm.reporting.pnl.explain.context</field>
</record>
<record model="ir.action.act_window.view" id="act_ctrm_pnl_explain_view">
<field name="sequence" eval="10"/>
<field name="view" ref="ctrm_pnl_explain_view_list"/>
<field name="act_window" ref="act_ctrm_pnl_explain"/>
</record>
<record model="ir.ui.view" id="ctrm_pnl_dimension_context_view_form">
<field name="model">ctrm.reporting.pnl.dimension.context</field>
<field name="type">form</field>
<field name="name">ctrm_pnl_dimension_context_form</field>
</record>
<record model="ir.ui.view" id="ctrm_pnl_dimension_view_list">
<field name="model">ctrm.reporting.pnl.dimension</field>
<field name="type">tree</field>
<field name="name">ctrm_pnl_dimension_list</field>
</record>
<record model="ir.action.act_window" id="act_ctrm_pnl_dimension">
<field name="name">P&amp;L by Dimension</field>
<field name="res_model">ctrm.reporting.pnl.dimension</field>
<field name="context_model">ctrm.reporting.pnl.dimension.context</field>
</record>
<record model="ir.action.act_window.view" id="act_ctrm_pnl_dimension_view">
<field name="sequence" eval="10"/>
<field name="view" ref="ctrm_pnl_dimension_view_list"/>
<field name="act_window" ref="act_ctrm_pnl_dimension"/>
</record>
<menuitem <menuitem
name="Positions" name="Positions"
@@ -151,11 +191,13 @@
name="P&amp;L Explain" name="P&amp;L Explain"
parent="menu_ctrm_pnl" parent="menu_ctrm_pnl"
sequence="30" sequence="30"
action="act_ctrm_pnl_explain"
id="menu_ctrm_pnl_explain"/> id="menu_ctrm_pnl_explain"/>
<menuitem <menuitem
name="P&amp;L by Dimension" name="P&amp;L by Dimension"
parent="menu_ctrm_pnl" parent="menu_ctrm_pnl"
sequence="40" sequence="40"
action="act_ctrm_pnl_dimension"
id="menu_ctrm_pnl_dimension"/> id="menu_ctrm_pnl_dimension"/>
<menuitem <menuitem

View File

@@ -0,0 +1,22 @@
<form>
<label name="date"/>
<field name="date"/>
<label name="dimension"/>
<field name="dimension"/>
<label name="value"/>
<field name="value"/>
<label name="product"/>
<field name="product"/>
<label name="counterparty"/>
<field name="counterparty"/>
<label name="currency"/>
<field name="currency"/>
<label name="purchase"/>
<field name="purchase"/>
<label name="sale"/>
<field name="sale"/>
<label name="strategy"/>
<field name="strategy"/>
<label name="state"/>
<field name="state"/>
</form>

View File

@@ -0,0 +1,14 @@
<tree>
<field name="valuation_date"/>
<field name="dimension" width="120"/>
<field name="value" width="140"/>
<field name="product" width="140"/>
<field name="counterparty" width="120"/>
<field name="state"/>
<field name="strategy"/>
<field name="quantity" symbol="unit" sum="1"/>
<field name="currency" width="60"/>
<field name="amount" sum="1"/>
<field name="mtm" sum="1"/>
<field name="mtm_delta" sum="1"/>
</tree>

View File

@@ -0,0 +1,18 @@
<form>
<label name="date"/>
<field name="date"/>
<label name="product"/>
<field name="product"/>
<label name="counterparty"/>
<field name="counterparty"/>
<label name="currency"/>
<field name="currency"/>
<label name="purchase"/>
<field name="purchase"/>
<label name="sale"/>
<field name="sale"/>
<label name="strategy"/>
<field name="strategy"/>
<label name="state"/>
<field name="state"/>
</form>

View File

@@ -0,0 +1,13 @@
<tree>
<field name="valuation_date"/>
<field name="product" width="140"/>
<field name="counterparty" width="120"/>
<field name="state"/>
<field name="strategy"/>
<field name="currency" width="60"/>
<field name="physical_pnl" sum="1"/>
<field name="fee_pnl" sum="1"/>
<field name="derivative_pnl" sum="1"/>
<field name="mtm_delta" sum="1"/>
<field name="total_pnl" sum="1"/>
</tree>

View File

@@ -34,3 +34,12 @@ Date: 2026-05-24
- toutes les lignes valorisees. - toutes les lignes valorisees.
- `Mark-to-Market` lit la derniere valuation et compare `mtm` a `amount` - `Mark-to-Market` lit la derniere valuation et compare `mtm` a `amount`
via une colonne `MTM Delta`. via une colonne `MTM Delta`.
- `P&L Explain` decompose le P&L en blocs auditables:
- physical;
- fees;
- derivatives;
- MTM delta.
- `P&L by Dimension` joint les dimensions analytiques portees par les achats
et ventes. Si un contrat porte plusieurs dimensions, une ligne peut etre
presente sur plusieurs axes; filtrer par dimension pour une analyse sans
double comptage inter-axes.