Permalink
Browse files

[REF] models: use `parent_path` to implement parent_store

This replaces the former modified preorder tree traversal (MPTT) with the
fields `parent_left`/`parent_right`.  Each record is associated to a string
`parent_path`, that represents the path from its root node to itself.  The path
is made of the node ids suffixed with a slash:

              a                 node | id | parent_path
             / \                  a  | 42 | 42/
           ...  b                 b  | 63 | 42/63/
               / \                c  | 84 | 42/63/84/
              c   d               d  | 85 | 42/63/85/

This field provides an efficient implementation for parent_of/child_of queries:
the nodes in the subtree of record are the ones where `parent_path` starts with
the `parent_path` of record.  It is also more efficient to maintain than the
MPTT fields, and less sensitive to concurrent updates, because the value of
`parent_path` does not depend on sibling nodes.
  • Loading branch information...
rco-odoo committed Jan 25, 2018
1 parent 80f1ac3 commit e724858d508a2a7a938455a49f7dc509f6eac4c6
@@ -310,13 +310,11 @@ def action_open_reconcile(self):
class AccountGroup(models.Model):
_name = "account.group"
_parent_store = True
_order = 'code_prefix'
parent_id = fields.Many2one('account.group', index=True, ondelete='cascade')
parent_left = fields.Integer('Left Parent', index=True)
parent_right = fields.Integer('Right Parent', index=True)
parent_path = fields.Char(index=True)
name = fields.Char(required=True)
code_prefix = fields.Char()
@@ -39,8 +39,7 @@ class AccountAnalyticGroup(models.Model):
name = fields.Char(required=True)
description = fields.Text(string='Description')
parent_id = fields.Many2one('account.analytic.group', string="Parent", ondelete='cascade')
parent_left = fields.Integer('Left Parent', index=True)
parent_right = fields.Integer('Right Parent', index=True)
parent_path = fields.Char(index=True)
children_ids = fields.One2many('account.analytic.group', 'parent_id', string="Childrens")
complete_name = fields.Char('Complete Name', compute='_compute_complete_name', store=True)
company_id = fields.Many2one('res.company', string='Company')
@@ -152,8 +152,6 @@ class StockWarehouse(models.Model):
region_id = fields.Many2one('l10n_be_intrastat.region', string='Intrastat region')
def get_regionid_from_locationid(self, location):
location_ids = location.search([('parent_left', '<=', location.parent_left), ('parent_right', '>=', location.parent_right)])
warehouses = self.search([('lot_stock_id', 'in', location_ids.ids), ('region_id', '!=', False)], limit=1)
if warehouses:
return warehouses.region_id.id
return None
domain = [('lot_stock_id', 'parent_of', location.ids), ('region_id', '!=', False)]
warehouses = self.search(domain, limit=1)
return warehouses.region_id.id or None
@@ -17,18 +17,16 @@ class ProductCategory(models.Model):
_description = "Product Category"
_parent_name = "parent_id"
_parent_store = True
_parent_order = 'name'
_rec_name = 'complete_name'
_order = 'parent_left'
_order = 'complete_name'
name = fields.Char('Name', index=True, required=True, translate=True)
complete_name = fields.Char(
'Complete Name', compute='_compute_complete_name',
store=True)
parent_id = fields.Many2one('product.category', 'Parent Category', index=True, ondelete='cascade')
parent_path = fields.Char(index=True)
child_id = fields.One2many('product.category', 'parent_id', 'Child Categories')
parent_left = fields.Integer('Left Parent', index=1)
parent_right = fields.Integer('Right Parent', index=1)
product_count = fields.Integer(
'# Products', compute='_compute_product_count',
help="The number of products under this category (Does not consider the children categories)")
@@ -151,7 +151,7 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False):
'AND (item.pricelist_id = %s) '
'AND (item.date_start IS NULL OR item.date_start<=%s) '
'AND (item.date_end IS NULL OR item.date_end>=%s)'
'ORDER BY item.applied_on, item.min_quantity desc, categ.parent_left desc',
'ORDER BY item.applied_on, item.min_quantity desc, categ.complete_name desc',
(prod_tmpl_ids, prod_ids, categ_ids, self.id, date, date))
item_ids = [x[0] for x in self._cr.fetchall()]
@@ -195,26 +195,23 @@ def _get_domain_locations_new(self, location_ids, company_id=False, compute_chil
domain = company_id and ['&', ('company_id', '=', company_id)] or []
locations = self.env['stock.location'].browse(location_ids)
# TDE FIXME: should move the support of child_of + auto_join directly in expression
# The code has been modified because having one location with parent_left being
# 0 make the whole domain unusable
hierarchical_locations = locations.filtered(lambda location: location.parent_left != 0 and operator == "child_of")
other_locations = locations.filtered(lambda location: location not in hierarchical_locations) # TDE: set - set ?
hierarchical_locations = locations if operator == 'child_of' else locations.browse()
other_locations = locations - hierarchical_locations
loc_domain = []
dest_loc_domain = []
# this optimizes [('location_id', 'child_of', hierarchical_locations.ids)]
# by avoiding the ORM to search for children locations and injecting a
# lot of location ids into the main query
for location in hierarchical_locations:
loc_domain = loc_domain and ['|'] + loc_domain or loc_domain
loc_domain += ['&',
('location_id.parent_left', '>=', location.parent_left),
('location_id.parent_left', '<', location.parent_right)]
loc_domain.append(('location_id.parent_path', '=like', location.parent_path + '%'))
dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain
dest_loc_domain += ['&',
('location_dest_id.parent_left', '>=', location.parent_left),
('location_dest_id.parent_left', '<', location.parent_right)]
dest_loc_domain.append(('location_dest_id.parent_path', '=like', location.parent_path + '%'))
if other_locations:
loc_domain = loc_domain and ['|'] + loc_domain or loc_domain
loc_domain = loc_domain + [('location_id', operator, [location.id for location in other_locations])]
loc_domain = loc_domain + [('location_id', operator, other_locations.ids)]
dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain
dest_loc_domain = dest_loc_domain + [('location_dest_id', operator, [location.id for location in other_locations])]
dest_loc_domain = dest_loc_domain + [('location_dest_id', operator, other_locations.ids)]
return (
domain + loc_domain,
domain + dest_loc_domain + ['!'] + loc_domain if loc_domain else domain + dest_loc_domain,
@@ -14,8 +14,7 @@ class Location(models.Model):
_description = "Inventory Locations"
_parent_name = "location_id"
_parent_store = True
_parent_order = 'name'
_order = 'parent_left'
_order = 'complete_name'
_rec_name = 'complete_name'
@api.model
@@ -55,8 +54,7 @@ def default_get(self, fields):
posx = fields.Integer('Corridor (X)', default=0, help="Optional localization details, for information purpose only")
posy = fields.Integer('Shelves (Y)', default=0, help="Optional localization details, for information purpose only")
posz = fields.Integer('Height (Z)', default=0, help="Optional localization details, for information purpose only")
parent_left = fields.Integer('Left Parent', index=True)
parent_right = fields.Integer('Right Parent', index=True)
parent_path = fields.Char(index=True)
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env['res.company']._company_default_get('stock.location'), index=True,
@@ -119,9 +117,8 @@ def get_putaway_strategy(self, product):
@api.returns('stock.warehouse', lambda value: value.id)
def get_warehouse(self):
""" Returns warehouse id of warehouse that contains location """
return self.env['stock.warehouse'].search([
('view_location_id.parent_left', '<=', self.parent_left),
('view_location_id.parent_right', '>=', self.parent_left)], limit=1)
domain = [('view_location_id', 'parent_of', self.ids)]
return self.env['stock.warehouse'].search(domain, limit=1)
def should_bypass_reservation(self):
self.ensure_one()
@@ -791,7 +791,6 @@ class Menu(models.Model):
_description = "Website Menu"
_parent_store = True
_parent_order = 'sequence'
_order = "sequence, id"
def _default_sequence(self):
@@ -806,8 +805,7 @@ def _default_sequence(self):
website_id = fields.Many2one('website', 'Website') # TODO: support multiwebsite once done for ir.ui.views
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
parent_left = fields.Integer('Parent Left', index=True)
parent_right = fields.Integer('Parent Right', index=True)
parent_path = fields.Char(index=True)
is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
@api.one
@@ -8,17 +8,15 @@ class Documentation(models.Model):
_name = 'forum.documentation.toc'
_description = 'Documentation ToC'
_inherit = ['website.seo.metadata']
_order = "parent_left"
_parent_order = "sequence, name"
_order = "sequence, name"
_parent_store = True
sequence = fields.Integer('Sequence')
name = fields.Char('Name', required=True, translate=True)
introduction = fields.Html('Introduction', translate=True)
parent_id = fields.Many2one('forum.documentation.toc', string='Parent Table Of Content', ondelete='cascade')
child_ids = fields.One2many('forum.documentation.toc', 'parent_id', string='Children Table Of Content')
parent_left = fields.Integer(string='Left Parent', index=True)
parent_right = fields.Integer(string='Right Parent', index=True)
parent_path = fields.Char(index=True)
post_ids = fields.One2many('forum.post', 'documentation_toc_id', string='Posts')
forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
@@ -29,8 +29,7 @@ def __init__(self, *args, **kwargs):
sequence = fields.Integer(default=10)
child_id = fields.One2many('ir.ui.menu', 'parent_id', string='Child IDs')
parent_id = fields.Many2one('ir.ui.menu', string='Parent Menu', index=True, ondelete="restrict")
parent_left = fields.Integer(index=True)
parent_right = fields.Integer(index=True)
parent_path = fields.Char(index=True)
groups_id = fields.Many2many('res.groups', 'ir_ui_menu_group_rel',
'menu_id', 'gid', string='Groups',
help="If you have groups, the visibility of this menu will be based on these groups. "\
@@ -67,17 +67,15 @@ def _fields_view_get_address(self, arch):
class PartnerCategory(models.Model):
_description = 'Partner Tags'
_name = 'res.partner.category'
_order = 'parent_left, name'
_order = 'name'
_parent_store = True
_parent_order = 'name'
name = fields.Char(string='Tag Name', required=True, translate=True)
color = fields.Integer(string='Color Index')
parent_id = fields.Many2one('res.partner.category', string='Parent Category', index=True, ondelete='cascade')
child_ids = fields.One2many('res.partner.category', 'parent_id', string='Child Tags')
active = fields.Boolean(default=True, help="The active field allows you to hide the category without removing it.")
parent_left = fields.Integer(string='Left parent', index=True)
parent_right = fields.Integer(string='Right parent', index=True)
parent_path = fields.Char(index=True)
partner_ids = fields.Many2many('res.partner', column1='category_id', column2='partner_id', string='Partners')
@api.constrains('parent_id')
@@ -13,13 +13,11 @@ class Category(models.Model):
_order = 'name'
_parent_store = True
_parent_name = 'parent'
_parent_order = 'name'
name = fields.Char(required=True)
color = fields.Integer('Color Index')
parent = fields.Many2one('test_new_api.category', ondelete='cascade')
parent_left = fields.Integer("Left Parent", index=True)
parent_right = fields.Integer("Right Parent", index=True)
parent_path = fields.Char(index=True)
root_categ = fields.Many2one(_name, compute='_compute_root_categ')
display_name = fields.Char(compute='_compute_display_name', inverse='_inverse_display_name')
dummy = fields.Char(store=False)
Oops, something went wrong.

0 comments on commit e724858

Please sign in to comment.