diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py
index f9d05da..2ed0876 100755
--- a/modules/purchase_trade/purchase.py
+++ b/modules/purchase_trade/purchase.py
@@ -2297,6 +2297,78 @@ class Line(metaclass=PoolMeta):
'get_premium', setter='set_premium')
fee_ = fields.Many2One('fee.fee',"Fee")
pricing_rule = fields.Text("Pricing description")
+ parent_trader = fields.Function(
+ fields.Many2One('party.party', "Trader"), 'get_parent_field')
+ parent_operator = fields.Function(
+ fields.Many2One('party.party', "Operator"), 'get_parent_field')
+ parent_broker = fields.Function(
+ fields.Many2One('party.party', "Broker"), 'get_parent_field')
+ parent_contract_template = fields.Function(
+ fields.Many2One('contract.template', "Contract Template"),
+ 'get_parent_field')
+ parent_our_reference = fields.Function(
+ fields.Char("Our Reference"), 'get_parent_field')
+ parent_lc_date = fields.Function(
+ fields.Date("LC date"), 'get_parent_field')
+ parent_product_origin = fields.Function(
+ fields.Char("Origin"), 'get_parent_field')
+ price_component_count = fields.Function(
+ fields.Integer("Components"), 'get_relation_count')
+ pricing_count = fields.Function(
+ fields.Integer("Pricing"), 'get_relation_count')
+ price_summary_count = fields.Function(
+ fields.Integer("Summary"), 'get_relation_count')
+ premium_decomposition_count = fields.Function(
+ fields.Integer("Premium lines"), 'get_relation_count')
+ mtm_count = fields.Function(
+ fields.Integer("MTM"), 'get_relation_count')
+ fee_count = fields.Function(
+ fields.Integer("Fees"), 'get_relation_count')
+ lot_count = fields.Function(
+ fields.Integer("Lots"), 'get_relation_count')
+ derivative_count = fields.Function(
+ fields.Integer("Derivatives"), 'get_relation_count')
+ estimated_date_count = fields.Function(
+ fields.Integer("Estimated dates"), 'get_relation_count')
+ charter_condition_count = fields.Function(
+ fields.Integer("Charter conditions"), 'get_relation_count')
+
+ _parent_field_map = {
+ 'parent_trader': 'trader',
+ 'parent_operator': 'operator',
+ 'parent_broker': 'broker',
+ 'parent_contract_template': 'contract_template',
+ 'parent_our_reference': 'our_reference',
+ 'parent_lc_date': 'lc_date',
+ 'parent_product_origin': 'product_origin',
+ }
+ _relation_count_map = {
+ 'price_component_count': 'price_components',
+ 'pricing_count': 'price_pricing',
+ 'price_summary_count': 'price_summary',
+ 'premium_decomposition_count': 'premium_decomposition',
+ 'mtm_count': 'mtm',
+ 'fee_count': 'fees',
+ 'lot_count': 'lots',
+ 'derivative_count': 'derivatives',
+ 'estimated_date_count': 'estimated_date',
+ 'charter_condition_count': 'charter_conditions',
+ }
+
+ def get_parent_field(self, name=None):
+ field_name = self._parent_field_map.get(name)
+ if not field_name or not self.purchase:
+ return None
+ value = getattr(self.purchase, field_name, None)
+ if hasattr(value, 'id'):
+ return value.id
+ return value
+
+ def get_relation_count(self, name=None):
+ field_name = self._relation_count_map.get(name)
+ if not field_name:
+ return 0
+ return len(getattr(self, field_name, None) or [])
@classmethod
def default_unit(cls):
diff --git a/modules/purchase_trade/purchase.xml b/modules/purchase_trade/purchase.xml
index 9b8b6eb..d4e0076 100755
--- a/modules/purchase_trade/purchase.xml
+++ b/modules/purchase_trade/purchase.xml
@@ -37,11 +37,31 @@ this repository contains the full copyright notices and license terms. -->
purchase_line_form
-
- purchase.line
-
- purchase_line_tree_sequence
-
+
+ purchase.line
+
+ purchase_line_tree_sequence
+
+
+ purchase.line
+ tree
+ purchase_line_reporting_tree
+
+
+ Purchase Lines
+ purchase.line
+
+
+
+
+
+
+
+
+
+
+
+
🧠Go to BI
@@ -406,5 +426,11 @@ this repository contains the full copyright notices and license terms. -->
sequence="16"
action="act_contract_template_form"
id="menu_purchase_contract_template_form"/>
+
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index d0c7c60..835d520 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -2144,6 +2144,87 @@ class SaleLine(metaclass=PoolMeta):
finished = fields.Boolean("Mark as finished")
pricing_rule = fields.Text("Pricing description")
price_composition = fields.One2Many('price.composition','sale_line',"Price composition")
+ parent_trader = fields.Function(
+ fields.Many2One('party.party', "Trader"), 'get_parent_field')
+ parent_operator = fields.Function(
+ fields.Many2One('party.party', "Operator"), 'get_parent_field')
+ parent_broker = fields.Function(
+ fields.Many2One('party.party', "Broker"), 'get_parent_field')
+ parent_contract_template = fields.Function(
+ fields.Many2One('contract.template', "Contract Template"),
+ 'get_parent_field')
+ parent_invoice_party = fields.Function(
+ fields.Many2One('party.party', "Invoice Party"), 'get_parent_field')
+ parent_shipment_party = fields.Function(
+ fields.Many2One('party.party', "Shipment Party"), 'get_parent_field')
+ parent_our_reference = fields.Function(
+ fields.Char("Our Reference"), 'get_parent_field')
+ parent_lc_date = fields.Function(
+ fields.Date("LC date"), 'get_parent_field')
+ parent_product_origin = fields.Function(
+ fields.Char("Origin"), 'get_parent_field')
+ price_component_count = fields.Function(
+ fields.Integer("Components"), 'get_relation_count')
+ pricing_count = fields.Function(
+ fields.Integer("Pricing"), 'get_relation_count')
+ price_summary_count = fields.Function(
+ fields.Integer("Summary"), 'get_relation_count')
+ price_composition_count = fields.Function(
+ fields.Integer("Price composition"), 'get_relation_count')
+ premium_decomposition_count = fields.Function(
+ fields.Integer("Premium lines"), 'get_relation_count')
+ mtm_count = fields.Function(
+ fields.Integer("MTM"), 'get_relation_count')
+ fee_count = fields.Function(
+ fields.Integer("Fees"), 'get_relation_count')
+ lot_count = fields.Function(
+ fields.Integer("Lots"), 'get_relation_count')
+ derivative_count = fields.Function(
+ fields.Integer("Derivatives"), 'get_relation_count')
+ estimated_date_count = fields.Function(
+ fields.Integer("Estimated dates"), 'get_relation_count')
+ charter_condition_count = fields.Function(
+ fields.Integer("Charter conditions"), 'get_relation_count')
+
+ _parent_field_map = {
+ 'parent_trader': 'trader',
+ 'parent_operator': 'operator',
+ 'parent_broker': 'broker',
+ 'parent_contract_template': 'contract_template',
+ 'parent_invoice_party': 'invoice_party',
+ 'parent_shipment_party': 'shipment_party',
+ 'parent_our_reference': 'our_reference',
+ 'parent_lc_date': 'lc_date',
+ 'parent_product_origin': 'product_origin',
+ }
+ _relation_count_map = {
+ 'price_component_count': 'price_components',
+ 'pricing_count': 'price_pricing',
+ 'price_summary_count': 'price_summary',
+ 'price_composition_count': 'price_composition',
+ 'premium_decomposition_count': 'premium_decomposition',
+ 'mtm_count': 'mtm',
+ 'fee_count': 'fees',
+ 'lot_count': 'lots',
+ 'derivative_count': 'derivatives',
+ 'estimated_date_count': 'estimated_date',
+ 'charter_condition_count': 'charter_conditions',
+ }
+
+ def get_parent_field(self, name=None):
+ field_name = self._parent_field_map.get(name)
+ if not field_name or not self.sale:
+ return None
+ value = getattr(self.sale, field_name, None)
+ if hasattr(value, 'id'):
+ return value.id
+ return value
+
+ def get_relation_count(self, name=None):
+ field_name = self._relation_count_map.get(name)
+ if not field_name:
+ return 0
+ return len(getattr(self, field_name, None) or [])
@classmethod
def default_finished(cls):
diff --git a/modules/purchase_trade/sale.xml b/modules/purchase_trade/sale.xml
index 029190a..4072871 100755
--- a/modules/purchase_trade/sale.xml
+++ b/modules/purchase_trade/sale.xml
@@ -13,11 +13,31 @@ this repository contains the full copyright notices and license terms. -->
sale_line_form
-
- sale.line
-
- sale_line_tree_sequence
-
+
+ sale.line
+
+ sale_line_tree_sequence
+
+
+ sale.line
+ tree
+ sale_line_reporting_tree
+
+
+ Sale Lines
+ sale.line
+
+
+
+
+
+
+
+
+
+
+
+
lot.lot
@@ -63,10 +83,16 @@ this repository contains the full copyright notices and license terms. -->
-
- sale.sale
- form
- sale_btb_form
-
-
+
+ sale.sale
+ form
+ sale_btb_form
+
+
+
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index f10926e..1cd67b0 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -4161,6 +4161,66 @@ class PurchaseTradeTestCase(ModuleTestCase):
'estimated_date': datetime.date(2026, 5, 1),
}])])
+ def test_purchase_line_reporting_parent_fields_and_counts(self):
+ 'purchase line reporting fields mirror parent values and relation counts'
+ PurchaseLine = Pool().get('purchase.line')
+ line = PurchaseLine()
+ line.purchase = Mock(
+ trader=Mock(id=10),
+ operator=Mock(id=11),
+ broker=Mock(id=12),
+ contract_template=Mock(id=13),
+ our_reference='OUR-PUR',
+ lc_date=datetime.date(2026, 5, 2),
+ product_origin='Origin',
+ )
+ line.price_components = [Mock(), Mock()]
+ line.price_pricing = [Mock()]
+ line.mtm = [Mock(), Mock(), Mock()]
+ line.lots = []
+
+ self.assertEqual(line.get_parent_field('parent_trader'), 10)
+ self.assertEqual(line.get_parent_field('parent_operator'), 11)
+ self.assertEqual(
+ line.get_parent_field('parent_contract_template'), 13)
+ self.assertEqual(line.get_parent_field('parent_our_reference'), 'OUR-PUR')
+ self.assertEqual(
+ line.get_relation_count('price_component_count'), 2)
+ self.assertEqual(line.get_relation_count('pricing_count'), 1)
+ self.assertEqual(line.get_relation_count('mtm_count'), 3)
+ self.assertEqual(line.get_relation_count('lot_count'), 0)
+
+ def test_sale_line_reporting_parent_fields_and_counts(self):
+ 'sale line reporting fields mirror parent values and relation counts'
+ SaleLine = Pool().get('sale.line')
+ line = SaleLine()
+ line.sale = Mock(
+ trader=Mock(id=20),
+ operator=Mock(id=21),
+ invoice_party=Mock(id=22),
+ shipment_party=Mock(id=23),
+ contract_template=Mock(id=24),
+ our_reference='OUR-SALE',
+ lc_date=datetime.date(2026, 5, 3),
+ product_origin='Origin',
+ )
+ line.price_components = [Mock()]
+ line.price_composition = [Mock(), Mock()]
+ line.price_pricing = []
+ line.fees = [Mock(), Mock(), Mock()]
+
+ self.assertEqual(line.get_parent_field('parent_trader'), 20)
+ self.assertEqual(line.get_parent_field('parent_operator'), 21)
+ self.assertEqual(line.get_parent_field('parent_invoice_party'), 22)
+ self.assertEqual(line.get_parent_field('parent_shipment_party'), 23)
+ self.assertEqual(line.get_parent_field('parent_our_reference'), 'OUR-SALE')
+ self.assertEqual(
+ line.get_relation_count('price_component_count'), 1)
+ self.assertEqual(
+ line.get_relation_count('price_composition_count'), 2)
+ self.assertEqual(line.get_relation_count('pricing_count'), 0)
+ self.assertEqual(line.get_relation_count('fee_count'), 3)
+
def test_purchase_and_sale_line_keep_explicit_estimated_bl_date(self):
'new lines keep explicit estimated BL date'
PurchaseLine = Pool().get('purchase.line')
diff --git a/modules/purchase_trade/view/purchase_line_reporting_tree.xml b/modules/purchase_trade/view/purchase_line_reporting_tree.xml
new file mode 100644
index 0000000..bd6e215
--- /dev/null
+++ b/modules/purchase_trade/view/purchase_line_reporting_tree.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/purchase_trade/view/sale_line_reporting_tree.xml b/modules/purchase_trade/view/sale_line_reporting_tree.xml
new file mode 100644
index 0000000..44691bf
--- /dev/null
+++ b/modules/purchase_trade/view/sale_line_reporting_tree.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+