From 858b7258d99c522633332999ed4ae67c95a2160f Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Wed, 8 Jul 2026 14:38:36 +0200 Subject: [PATCH] Pnl graph --- modules/purchase_trade/__init__.py | 13 ++-- modules/purchase_trade/purchase.py | 64 +++++++++++++++---- modules/purchase_trade/tests/test_module.py | 19 ++++++ modules/purchase_trade/view/purchase_form.xml | 12 +++- 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index d9dd13b..ffd2511 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -185,12 +185,13 @@ def register(): derivative.DerivativeReportContext, fee.FeeReport, fee.FeeContext, - forex.ForexCoverPhysicalContract, - forex.PForex, - forex.ForexBI, - purchase.PnlBI, - purchase.PositionBI, - stock.Move, + forex.ForexCoverPhysicalContract, + forex.PForex, + forex.ForexBI, + purchase.PurchasePnlGraph, + purchase.PnlBI, + purchase.PositionBI, + stock.Move, stock.Location, stock.InvoiceLine, stock.ShipmentIn, diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index acf77b6..b8be77a 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -11,7 +11,7 @@ from trytond.tools import (cursor_dict, 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.conditionals import Case, Coalesce from sql import Column, Literal from sql.functions import CurrentTimestamp, DateTrunc, Abs from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction @@ -370,11 +370,13 @@ class Purchase(metaclass=PoolMeta): wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True) association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),}) crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),}) - pnl = fields.One2Many('valuation.valuation.dyn', 'r_purchase', 'Pnl',states={'invisible': ~Eval('group_pnl'),}) - pnl_ = fields.One2Many('valuation.valuation.line', 'purchase', 'Pnl',states={'invisible': Eval('group_pnl'),}) - derivatives = fields.One2Many('derivative.derivative', 'purchase', 'Derivative') - plans = fields.One2Many('workflow.plan','purchase',"Execution plans") - forex = fields.One2Many('forex.cover.physical.contract','contract',"Forex",readonly=True) + pnl = fields.One2Many('valuation.valuation.dyn', 'r_purchase', 'Pnl',states={'invisible': ~Eval('group_pnl'),}) + pnl_ = fields.One2Many('valuation.valuation.line', 'purchase', 'Pnl',states={'invisible': Eval('group_pnl'),}) + pnl_graph = fields.One2Many( + 'purchase.pnl.graph', 'purchase', 'Pnl graph') + derivatives = fields.One2Many('derivative.derivative', 'purchase', 'Derivative') + plans = fields.One2Many('workflow.plan','purchase',"Execution plans") + forex = fields.One2Many('forex.cover.physical.contract','contract',"Forex",readonly=True) plan = fields.Many2One('workflow.plan',"Name") estimated_date = fields.One2Many('pricing.estimated','purchase',"Estimated date") group_pnl = fields.Boolean("Group Pnl") @@ -3319,9 +3321,9 @@ class GoToBi(Wizard): action['url'] = config.bi + '/dashboard/6-pnl?lot=&product=&purchase='+ ct_number + '&sale=' return action, {} -class PurchaseAllocationsWizard(Wizard): - 'Open Allocations report from Purchase without modal' - __name__ = 'purchase.allocations.wizard' +class PurchaseAllocationsWizard(Wizard): + 'Open Allocations report from Purchase without modal' + __name__ = 'purchase.allocations.wizard' start_state = 'open_report' @@ -3334,11 +3336,45 @@ class PurchaseAllocationsWizard(Wizard): action['context_model'] = 'lot.context' action['pyson_context'] = PYSONEncoder().encode({ 'purchase': purchase_id, - }) - return action, {} - -class PurchaseInvoiceReport( - ModelSQL, ModelView): + }) + return action, {} + + +class PurchasePnlGraph(ModelSQL, ModelView): + "Purchase Pnl graph" + __name__ = 'purchase.pnl.graph' + + purchase = fields.Many2One('purchase.purchase', "Purchase") + valuation_date = fields.Date("Valuation Date") + pnl = fields.Numeric("Pnl", digits=(16, 2)) + mtm = fields.Numeric("Mtm", digits=(16, 2)) + + @classmethod + def __setup__(cls): + super().__setup__() + cls._order.insert(0, ('valuation_date', 'ASC')) + + @classmethod + def table_query(cls): + Valuation = Pool().get('valuation.valuation') + val = Valuation.__table__() + return 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'), + val.purchase.as_('purchase'), + val.date.as_('valuation_date'), + Sum(Coalesce(val.pnl, 0)).as_('pnl'), + Sum(Coalesce(val.mtm, 0)).as_('mtm'), + where=(val.purchase != None), + group_by=[val.purchase, val.date], + order_by=[val.date.asc]) + + +class PurchaseInvoiceReport( + ModelSQL, ModelView): "Purchase invoices" __name__ = 'purchase.invoice.report' r_supplier = fields.Many2One('party.party',"Supplier") diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 31b795c..c7baf95 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2,6 +2,7 @@ # this repository contains the full copyright notices and license terms. import datetime +from pathlib import Path from contextlib import nullcontext from decimal import Decimal from types import SimpleNamespace @@ -5252,6 +5253,24 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual( ShipmentIn.operator.domain, [('categories.name', '=', 'OPERATOR')]) + def test_purchase_form_has_pnl_graph_tab_after_pnl(self): + 'purchase form exposes a rich graph for historical PnL and MTM' + path = Path('modules/purchase_trade/view/purchase_form.xml') + root = ElementTree.parse(path).getroot() + pages = root.findall(".//xpath[@expr=\"/form/notebook/page[@id='info']\"]//page") + page_ids = [page.get('id') for page in pages] + pnl_index = page_ids.index('pnl') + + self.assertEqual(page_ids[pnl_index + 1], 'pnl_graph') + + graph_field = pages[pnl_index + 1].find("field[@name='pnl_graph']") + self.assertIsNotNone(graph_field) + self.assertEqual(graph_field.get('widget'), 'rich_graph') + self.assertEqual(graph_field.get('date_field'), 'valuation_date') + self.assertEqual(graph_field.get('series'), 'pnl,mtm') + self.assertEqual(graph_field.get('labels'), 'pnl:Pnl,mtm:Mtm') + self.assertIsNone(graph_field.get('hidden_series')) + def test_shipment_in_pnl_lines_use_physical_and_open_lots(self): 'shipment PnL uses valuation lines from physical and open lot links' ShipmentIn = Pool().get('stock.shipment.in') diff --git a/modules/purchase_trade/view/purchase_form.xml b/modules/purchase_trade/view/purchase_form.xml index 5c7f729..79841eb 100755 --- a/modules/purchase_trade/view/purchase_form.xml +++ b/modules/purchase_trade/view/purchase_form.xml @@ -149,13 +149,23 @@ this repository contains the full copyright notices and license terms. --> - + + + +