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. -->
-
+
+
+
+