Purchase & Sale Lines

This commit is contained in:
2026-06-28 13:43:26 +02:00
parent f45cdb0807
commit 104d5c616c
7 changed files with 394 additions and 16 deletions

View File

@@ -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):

View File

@@ -37,11 +37,31 @@ this repository contains the full copyright notices and license terms. -->
<field name="inherit" ref="purchase.purchase_line_view_form"/>
<field name="name">purchase_line_form</field>
</record>
<record model="ir.ui.view" id="purchase_line_view_tree_sequence">
<field name="model">purchase.line</field>
<field name="inherit" ref="purchase.purchase_line_view_tree_sequence"/>
<field name="name">purchase_line_tree_sequence</field>
</record>
<record model="ir.ui.view" id="purchase_line_view_tree_sequence">
<field name="model">purchase.line</field>
<field name="inherit" ref="purchase.purchase_line_view_tree_sequence"/>
<field name="name">purchase_line_tree_sequence</field>
</record>
<record model="ir.ui.view" id="purchase_line_reporting_view_tree">
<field name="model">purchase.line</field>
<field name="type">tree</field>
<field name="name">purchase_line_reporting_tree</field>
</record>
<record model="ir.action.act_window" id="act_purchase_line_reporting">
<field name="name">Purchase Lines</field>
<field name="res_model">purchase.line</field>
<field name="domain" eval="[('type', '=', 'line')]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_purchase_line_reporting_view_tree">
<field name="sequence" eval="10"/>
<field name="view" ref="purchase_line_reporting_view_tree"/>
<field name="act_window" ref="act_purchase_line_reporting"/>
</record>
<record model="ir.action.act_window.view" id="act_purchase_line_reporting_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="purchase_line_view_form"/>
<field name="act_window" ref="act_purchase_line_reporting"/>
</record>
<record model="ir.action.wizard" id="act_bi">
<field name="name">🧠 Go to BI</field>
@@ -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"/>
<menuitem
name="Purchase Lines"
parent="purchase.menu_purchase"
sequence="17"
action="act_purchase_line_reporting"
id="menu_purchase_line_reporting"/>
</data>
</tryton>

View File

@@ -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):

View File

@@ -13,11 +13,31 @@ this repository contains the full copyright notices and license terms. -->
<field name="inherit" ref="sale.sale_line_view_form"/>
<field name="name">sale_line_form</field>
</record>
<record model="ir.ui.view" id="sale_line_view_tree_sequence">
<field name="model">sale.line</field>
<field name="inherit" ref="sale.sale_line_view_tree_sequence"/>
<field name="name">sale_line_tree_sequence</field>
</record>
<record model="ir.ui.view" id="sale_line_view_tree_sequence">
<field name="model">sale.line</field>
<field name="inherit" ref="sale.sale_line_view_tree_sequence"/>
<field name="name">sale_line_tree_sequence</field>
</record>
<record model="ir.ui.view" id="sale_line_reporting_view_tree">
<field name="model">sale.line</field>
<field name="type">tree</field>
<field name="name">sale_line_reporting_tree</field>
</record>
<record model="ir.action.act_window" id="act_sale_line_reporting">
<field name="name">Sale Lines</field>
<field name="res_model">sale.line</field>
<field name="domain" eval="[('type', '=', 'line')]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_sale_line_reporting_view_tree">
<field name="sequence" eval="10"/>
<field name="view" ref="sale_line_reporting_view_tree"/>
<field name="act_window" ref="act_sale_line_reporting"/>
</record>
<record model="ir.action.act_window.view" id="act_sale_line_reporting_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="sale_line_view_form"/>
<field name="act_window" ref="act_sale_line_reporting"/>
</record>
<record model="ir.ui.view" id="sale_lot_view_tree_sequence">
<field name="model">lot.lot</field>
@@ -63,10 +83,16 @@ this repository contains the full copyright notices and license terms. -->
<field name="action" ref="act_sale_allocations_wizard"/>
</record>
<record model="ir.ui.view" id="sale_btb_view_form">
<field name="model">sale.sale</field>
<field name="type">form</field>
<field name="name">sale_btb_form</field>
</record>
</data>
<record model="ir.ui.view" id="sale_btb_view_form">
<field name="model">sale.sale</field>
<field name="type">form</field>
<field name="name">sale_btb_form</field>
</record>
<menuitem
name="Sale Lines"
parent="sale.menu_sale"
sequence="17"
action="act_sale_line_reporting"
id="menu_sale_line_reporting"/>
</data>
</tryton>

View File

@@ -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')

View File

@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<tree>
<field name="purchase" expand="1"/>
<field name="purchase_state" widget="badge"
badge_colors="draft:#64748b,quotation:#2563eb,confirmed:#16a34a,processing:#f59e0b,done:#0f766e,cancelled:#ef4444,*:#94a3b8"/>
<field name="purchase_date"/>
<field name="supplier" expand="1"/>
<field name="parent_trader" optional="1"/>
<field name="parent_operator" optional="1"/>
<field name="parent_broker" optional="1"/>
<field name="parent_our_reference" optional="1"/>
<field name="parent_contract_template" optional="1"/>
<field name="parent_lc_date" optional="1"/>
<field name="product" expand="1"/>
<field name="product_supplier" optional="1"/>
<field name="attributes_name" optional="1"/>
<field name="parent_product_origin" optional="1"/>
<field name="del_period"/>
<field name="from_del"/>
<field name="to_del"/>
<field name="period_at" optional="1"/>
<field name="from_location" optional="1"/>
<field name="to_location" optional="1"/>
<field name="quantity_theorical" symbol="unit"/>
<field name="quantity" symbol="unit"/>
<field name="actual_quantity" symbol="unit" optional="1"/>
<field name="targeted_qt" symbol="unit" optional="1"/>
<field name="unit" optional="1"/>
<field name="price_type" widget="badge"
badge_colors="priced:#16a34a,basis:#2563eb,efp:#8b5cf6,cash:#f59e0b,*:#94a3b8"/>
<field name="unit_price"/>
<field name="premium" optional="1"/>
<field name="amount"/>
<field name="currency"/>
<field name="progress" widget="progressbar" optional="1"/>
<field name="moves_progress" string="Shipping" widget="progressbar" optional="1"/>
<field name="invoice_progress" string="Invoicing" widget="progressbar" optional="1"/>
<field name="enable_linked_currency" optional="1"/>
<field name="linked_price" optional="1"/>
<field name="linked_currency" optional="1"/>
<field name="linked_unit" optional="1"/>
<field name="price_component_count" optional="1"/>
<field name="pricing_count" optional="1"/>
<field name="price_summary_count" optional="1"/>
<field name="premium_decomposition_count" optional="1"/>
<field name="mtm_count" optional="1"/>
<field name="fee_count" optional="1"/>
<field name="lot_count" optional="1"/>
<field name="derivative_count" optional="1"/>
<field name="estimated_date_count" optional="1"/>
<field name="charter_condition_count" optional="1"/>
<field name="pricing_rule" optional="1"/>
<field name="certif" optional="1"/>
<field name="finished" optional="1"/>
</tree>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0"?>
<tree>
<field name="sale" expand="1"/>
<field name="sale_state" widget="badge"
badge_colors="draft:#64748b,quotation:#2563eb,confirmed:#16a34a,processing:#f59e0b,done:#0f766e,cancelled:#ef4444,*:#94a3b8"/>
<field name="sale_date"/>
<field name="customer" expand="1"/>
<field name="parent_invoice_party" optional="1"/>
<field name="parent_shipment_party" optional="1"/>
<field name="parent_trader" optional="1"/>
<field name="parent_operator" optional="1"/>
<field name="parent_broker" optional="1"/>
<field name="parent_our_reference" optional="1"/>
<field name="parent_contract_template" optional="1"/>
<field name="parent_lc_date" optional="1"/>
<field name="product" expand="1"/>
<field name="attributes_name" optional="1"/>
<field name="parent_product_origin" optional="1"/>
<field name="del_period"/>
<field name="from_del"/>
<field name="to_del"/>
<field name="period_at" optional="1"/>
<field name="shipping_date" optional="1"/>
<field name="from_location" optional="1"/>
<field name="to_location" optional="1"/>
<field name="quantity_theorical" symbol="unit"/>
<field name="quantity" symbol="unit"/>
<field name="actual_quantity" symbol="unit" optional="1"/>
<field name="targeted_qt" symbol="unit" optional="1"/>
<field name="unit" optional="1"/>
<field name="price_type" widget="badge"
badge_colors="priced:#16a34a,basis:#2563eb,efp:#8b5cf6,cash:#f59e0b,*:#94a3b8"/>
<field name="unit_price"/>
<field name="premium" optional="1"/>
<field name="amount"/>
<field name="currency"/>
<field name="progress" widget="progressbar" optional="1"/>
<field name="moves_progress" string="Shipping" widget="progressbar" optional="1"/>
<field name="invoice_progress" string="Invoicing" widget="progressbar" optional="1"/>
<field name="enable_linked_currency" optional="1"/>
<field name="linked_price" optional="1"/>
<field name="linked_currency" optional="1"/>
<field name="linked_unit" optional="1"/>
<field name="price_component_count" optional="1"/>
<field name="pricing_count" optional="1"/>
<field name="price_summary_count" optional="1"/>
<field name="price_composition_count" optional="1"/>
<field name="premium_decomposition_count" optional="1"/>
<field name="mtm_count" optional="1"/>
<field name="fee_count" optional="1"/>
<field name="lot_count" optional="1"/>
<field name="derivative_count" optional="1"/>
<field name="estimated_date_count" optional="1"/>
<field name="charter_condition_count" optional="1"/>
<field name="pricing_rule" optional="1"/>
<field name="certif" optional="1"/>
<field name="finished" optional="1"/>
</tree>