[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.
This commit is contained in:
Raphael Collet
2018-02-28 10:33:44 +01:00
parent 80f1ac3599
commit e724858d50
14 changed files with 133 additions and 221 deletions
+1 -3
View File
@@ -310,13 +310,11 @@ class AccountAccount(models.Model):
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()
+1 -2
View File
@@ -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
+2 -4
View File
@@ -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)")
+1 -1
View File
@@ -151,7 +151,7 @@ class Pricelist(models.Model):
'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()]
+9 -12
View File
@@ -195,26 +195,23 @@ class Product(models.Model):
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,
+4 -7
View File
@@ -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 @@ class Location(models.Model):
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 @@ class Location(models.Model):
@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()
+1 -3
View File
@@ -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 @@ class Menu(models.Model):
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,8 +8,7 @@ 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')
@@ -17,8 +16,7 @@ class Documentation(models.Model):
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)
+1 -2
View File
@@ -29,8 +29,7 @@ class IrUiMenu(models.Model):
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. "\
+2 -4
View File
@@ -67,17 +67,15 @@ class FormatAddressMixin(models.AbstractModel):
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')
+1 -3
View File
@@ -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)
+92 -155
View File
@@ -239,8 +239,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
_rec_name = None # field to use for labeling records
_order = 'id' # default order for searching results
_parent_name = 'parent_id' # the many2one field used as parent field
_parent_store = False # set to True to compute MPTT (parent_left, parent_right)
_parent_order = False # order to use for siblings in MPTT
_parent_store = False # set to True to compute parent_path field
_date_name = 'date' # field to use for default calendar view
_fold_name = 'fold' # field to determine folded groups in kanban views
@@ -2001,31 +2000,41 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
@api.model_cr
def _parent_store_compute(self):
""" Compute parent_path field from scratch. """
if not self._parent_store:
return
_logger.info('Computing parent left and right for table %s...', self._table)
cr = self._cr
select = "SELECT id FROM %s WHERE %s=%%s ORDER BY %s" % \
(self._table, self._parent_name, self._parent_order)
update = "UPDATE %s SET parent_left=%%s, parent_right=%%s WHERE id=%%s" % self._table
def process(root, left):
""" Set root.parent_left to ``left``, and return root.parent_right + 1 """
cr.execute(select, (root,))
right = left + 1
for (id,) in cr.fetchall():
right = process(id, right)
cr.execute(update, (left, right, root))
return right + 1
select0 = "SELECT id FROM %s WHERE %s IS NULL ORDER BY %s" % \
(self._table, self._parent_name, self._parent_order)
cr.execute(select0)
pos = 0
for (id,) in cr.fetchall():
pos = process(id, pos)
self.invalidate_cache(['parent_left', 'parent_right'])
# Each record is associated to a string 'parent_path', that represents
# the path from the record's root node to the record. The path is made
# of the node ids suffixed with a slash (see example below). The nodes
# in the subtree of record are the ones where 'parent_path' starts with
# the 'parent_path' of record.
#
# 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/
#
# Note: the final '/' is necessary to match subtrees correctly: '42/63'
# is a prefix of '42/630', but '42/63/' is not a prefix of '42/630/'.
_logger.info('Computing parent_path for table %s...', self._table)
query = """
WITH RECURSIVE __parent_store_compute(id, parent_path) AS (
SELECT row.id, concat(row.id, '/')
FROM {table} row
WHERE row.{parent} IS NULL
UNION
SELECT row.id, concat(comp.parent_path, row.id, '/')
FROM {table} row, __parent_store_compute comp
WHERE row.{parent} = comp.id
)
UPDATE {table} row SET parent_path = comp.parent_path
FROM __parent_store_compute comp
WHERE row.id = comp.id
""".format(table=self._table, parent=self._parent_name)
self.env.cr.execute(query)
self.invalidate_cache(['parent_path'])
return True
@api.model_cr
@@ -2119,7 +2128,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
tools.create_model_table(cr, self._table, self._description)
if self._parent_store:
if not tools.column_exists(cr, self._table, 'parent_left'):
if not tools.column_exists(cr, self._table, 'parent_path'):
self._create_parent_columns()
self._check_removed_columns(log=False)
@@ -2158,18 +2167,11 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
@api.model_cr
def _create_parent_columns(self):
tools.create_column(self._cr, self._table, 'parent_left', 'INTEGER')
tools.create_column(self._cr, self._table, 'parent_right', 'INTEGER')
if 'parent_left' not in self._fields:
_logger.error("add a field parent_left on model %s: parent_left = fields.Integer('Left Parent', index=True)", self._name)
elif not self._fields['parent_left'].index:
_logger.error('parent_left field on model %s must be indexed! Add index=True to the field definition)', self._name)
if 'parent_right' not in self._fields:
_logger.error("add a field parent_right on model %s: parent_right = fields.Integer('Left Parent', index=True)", self._name)
elif not self._fields['parent_right'].index:
_logger.error("parent_right field on model %s must be indexed! Add index=True to the field definition)", self._name)
if self._fields[self._parent_name].ondelete not in ('cascade', 'restrict'):
_logger.error("The field %s on model %s must be set as ondelete='cascade' or 'restrict'", self._parent_name, self._name)
tools.create_column(self._cr, self._table, 'parent_path', 'VARCHAR')
if 'parent_path' not in self._fields:
_logger.error("add a field parent_path on model %s: parent_path = fields.Char(index=True)", self._name)
elif not self._fields['parent_path'].index:
_logger.error('parent_path field on model %s must be indexed! Add index=True to the field definition)', self._name)
@api.model_cr
def _add_sql_constraints(self):
@@ -2393,10 +2395,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
elif 'x_name' in cls._fields:
cls._rec_name = 'x_name'
# make sure parent_order is set when necessary
if cls._parent_store and not cls._parent_order:
cls._parent_order = cls._order
@api.model
def fields_get(self, allfields=None, attributes=None):
""" fields_get([fields][, attributes])
@@ -2977,7 +2975,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self._check_concurrency()
self.check_access_rights('write')
bad_names = {'id', 'parent_left', 'parent_right'}
bad_names = {'id', 'parent_path'}
if self._log_access:
bad_names.update(LOG_ACCESS_COLUMNS)
@@ -3056,7 +3054,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cr = self._cr
# determine records that require updating parent_left, parent_right
# determine records that require updating parent_path
parent_records = self._parent_store_update_prepare(vals)
# determine SQL values
@@ -3145,7 +3143,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# check Python constraints
self._validate_fields(vals)
# update parent_left/parent_right
# update parent_path
if parent_records:
parent_records._parent_store_update(vals)
@@ -3178,7 +3176,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# add missing defaults
vals = self._add_missing_default_values(vals)
bad_names = {'id', 'parent_left', 'parent_right'}
bad_names = {'id', 'parent_path'}
if self._log_access:
bad_names.update(LOG_ACCESS_COLUMNS)
@@ -3250,9 +3248,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if field.type == 'boolean' and field.store:
vals.setdefault(name, False)
# prepare the update of parent_left, parent_right
parent_store = self._parent_store_create_prepare(vals)
# determine SQL values
columns = [] # list of (column_name, format, value)
other_fields = [] # list of non-column fields
@@ -3293,9 +3288,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# from now on, self is the new record
self = self.browse(cr.fetchone()[0])
# update parent_left, parent_right
if parent_store:
self._parent_store_update(vals)
# update parent_path
self._parent_store_create(vals)
with self.env.protecting(protected_fields, self):
# mark fields to recompute; do this before setting other fields,
@@ -3332,133 +3326,76 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return self
def _parent_store_create_prepare(self, vals):
""" Prepare the creation of a record, and return whether its
parent_left/parent_right fields must be updated after creation.
"""
def _parent_store_create(self, vals):
""" Set the parent_path field on ``self`` after its creation. """
if not self._parent_store:
return False
return
# temporarily put the node at the top-level rightmost position
query = "SELECT COALESCE(MAX(parent_right) + 1, 0) FROM {}"
self._cr.execute(query.format(self._table))
parent_left = self._cr.fetchone()[0]
vals.setdefault(self._parent_name, False)
vals['parent_left'] = parent_left
vals['parent_right'] = parent_left + 1
return True
parent_val = vals.get(self._parent_name)
if parent_val:
query = """
UPDATE {0}
SET parent_path=concat((SELECT parent_path FROM {0} WHERE id=%s), id, '/')
WHERE id IN %s
"""
params = [parent_val, tuple(self.ids)]
else:
query = "UPDATE {} SET parent_path=concat(id, '/') WHERE id IN %s"
params = [tuple(self.ids)]
self._cr.execute(query.format(self._table), params)
def _parent_store_update_prepare(self, vals):
""" Return the records in ``self`` that must update their parent_left,
parent_right fields, in their sibling order. This must be called
before updating the parent field.
""" Return the records in ``self`` that must update their parent_path
field. This must be called before updating the parent field.
"""
if not self._parent_store or self._parent_name not in vals:
return self.browse()
# The parent_left/right computation may take up to 5 seconds. No need to
# recompute the values if the parent is the same.
#
# Note: to respect parent_order, nodes must be processed in order, so
# the result must be ordered properly.
# No need to recompute the values if the parent is the same.
parent_val = vals[self._parent_name]
if parent_val:
query = """ SELECT id FROM {0}
WHERE id IN %s AND ({1} != %s OR {1} IS NULL)
ORDER BY {2} """
WHERE id IN %s AND ({1} != %s OR {1} IS NULL) """
params = [tuple(self.ids), parent_val]
else:
query = """ SELECT id FROM {0}
WHERE id IN %s AND {1} IS NOT NULL
ORDER BY {2} """
WHERE id IN %s AND {1} IS NOT NULL """
params = [tuple(self.ids)]
query = query.format(self._table, self._parent_name, self._parent_order)
query = query.format(self._table, self._parent_name)
self._cr.execute(query, params)
return self.browse([row[0] for row in self._cr.fetchall()])
def _parent_store_update(self, vals):
""" Update the parent_left/parent_right fields of ``self``. """
""" Update the parent_path field of ``self``. """
cr = self.env.cr
# determine new prefix in parent_path, and check for recursion
parent_val = vals[self._parent_name]
if parent_val:
clause, params = '{}=%s'.format(self._parent_name), [parent_val]
query = "SELECT parent_path FROM {} WHERE id=%s"
cr.execute(query.format(self._table), [parent_val])
new_prefix = cr.fetchone()[0]
parent_ids = {int(label) for label in new_prefix.split('/')[:-1]}
if not parent_ids.isdisjoint(self._ids):
raise UserError(_("Recursivity Detected."))
else:
clause, params = '{} IS NULL'.format(self._parent_name), []
new_prefix = ''
modified_ids = set()
for record in self:
# retrieve record's siblings data (this CANNOT be fetched
# outside the loop, as it needs to be refreshed after each
# update, in case several nodes are sequentially inserted one
# next to the other)
query = """ SELECT id, parent_left, parent_right FROM {}
WHERE {} ORDER BY {} """
cr.execute(query.format(self._table, clause, self._parent_order), params)
siblings = cr.fetchall()
# determine record's position among its siblings
index = next(pos
for pos, data in enumerate(siblings)
if data[0] == record.id)
# retrieve record's current data
pleft0, pright0 = siblings[index][1:]
width = pright0 - pleft0 + 1
# determine new parent_left of current record
if index:
# record comes right after its closest left sibling
pleft1 = (siblings[index - 1][2] or 0) + 1
else:
# record is the first node of its parent
if not parent_val:
pleft1 = 0 # the first node starts at 0
else:
query = "SELECT parent_left FROM {} WHERE id=%s"
cr.execute(query.format(self._table), [parent_val])
pleft1 = cr.fetchone()[0] + 1
if pleft0 == pleft1:
continue
if pleft0 < pleft1 <= pright0:
raise UserError(_('Recursivity Detected.'))
# We have to slide values in the interval [x,x+a+b) in order to
# swap the subsets in [x,x+a) and [x+a,x+a+b), respectively.
#
# x x+a x+b x+a+b
# |-----------|-----------|-----------|
# before: A A A A A A B B B B B B B B B B B B
# after: B B B B B B B B B B B B A A A A A A
#
query1 = """
UPDATE {}
SET parent_left = parent_left + (CASE
WHEN parent_left<%(x)s THEN 0
WHEN parent_left<%(x)s+%(a)s THEN %(b)s
WHEN parent_left<%(x)s+%(a)s+%(b)s THEN -%(a)s
ELSE 0 END),
parent_right = parent_right + (CASE
WHEN parent_right<%(x)s THEN 0
WHEN parent_right<%(x)s+%(a)s THEN %(b)s
WHEN parent_right<%(x)s+%(a)s+%(b)s THEN -%(a)s
ELSE 0 END)
WHERE (parent_left BETWEEN %(x)s AND %(x)s+%(a)s+%(b)s-1)
OR (parent_right BETWEEN %(x)s AND %(x)s+%(a)s+%(b)s-1)
RETURNING id
"""
if pleft0 < pleft1:
# x = pleft0, a = width, x+a+b = pleft1
params1 = dict(x=pleft0, a=width, b=pleft1 - pleft0 - width)
else:
# x = pleft1, x+a = pleft0, b = width
params1 = dict(x=pleft1, a=pleft0 - pleft1, b=width)
cr.execute(query1.format(self._table), params1)
modified_ids.update(row[0] for row in cr.fetchall())
self.browse(modified_ids).modified(['parent_left', 'parent_right'])
# update parent_path of all records and their descendants
query = """
UPDATE {0} child
SET parent_path = concat(%s, substr(child.parent_path,
length(node.parent_path) - length(node.id || '/') + 1))
FROM {0} node
WHERE node.id IN %s
AND child.parent_path LIKE concat(node.parent_path, '%%')
RETURNING child.id
"""
cr.execute(query.format(self._table), [new_prefix, tuple(self.ids)])
modified_ids = {row[0] for row in cr.fetchall()}
self.browse(modified_ids).modified(['parent_path'])
# TODO: ameliorer avec NULL
@api.model
@@ -3746,7 +3683,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
default['state'] = value
# build a black list of fields that should not be copied
blacklist = set(MAGIC_COLUMNS + ['parent_left', 'parent_right'])
blacklist = set(MAGIC_COLUMNS + ['parent_path'])
whitelist = set(name for name, field in self._fields.items() if not field.inherited)
def blacklist_given_fields(model):
+13 -16
View File
@@ -737,18 +737,15 @@ class expression(object):
def child_of_domain(left, ids, left_model, parent=None, prefix=''):
""" Return a domain implementing the child_of operator for [(left,child_of,ids)],
either as a range using the parent_left/right tree lookup fields
either as a range using the parent_path tree lookup field
(when available), or as an expanded [(left,in,child_ids)] """
if not ids:
return FALSE_DOMAIN
if left_model._parent_store:
# TODO: Improve where joins are implemented for many with '.', replace by:
# doms += ['&',(prefix+'.parent_left','<',rec.parent_right),(prefix+'.parent_left','>=',rec.parent_left)]
doms = []
for rec in left_model.browse(ids):
if doms:
doms.insert(0, OR_OPERATOR)
doms += [AND_OPERATOR, ('parent_left', '<', rec.parent_right), ('parent_left', '>=', rec.parent_left)]
doms = OR([
[('parent_path', '=like', rec.parent_path + '%')]
for rec in left_model.browse(ids)
])
if prefix:
return [(left, 'in', left_model.search(doms).ids)]
return doms
@@ -762,17 +759,17 @@ class expression(object):
def parent_of_domain(left, ids, left_model, parent=None, prefix=''):
""" Return a domain implementing the parent_of operator for [(left,parent_of,ids)],
either as a range using the parent_left/right tree lookup fields
either as a range using the parent_path tree lookup field
(when available), or as an expanded [(left,in,parent_ids)] """
if left_model._parent_store:
doms = []
for rec in left_model.browse(ids):
if doms:
doms.insert(0, OR_OPERATOR)
doms += [AND_OPERATOR, ('parent_right', '>', rec.parent_left), ('parent_left', '<=', rec.parent_left)]
parent_ids = [
int(label)
for rec in left_model.browse(ids)
for label in rec.parent_path.split('/')[:-1]
]
if prefix:
return [(left, 'in', left_model.search(doms).ids)]
return doms
return [(left, 'in', parent_ids)]
return [('id', 'in', parent_ids)]
else:
parent_name = parent or left_model._parent_name
parent_ids = set()