diff --git a/openerp/addons/base/tests/test_osv_expression.yml b/openerp/addons/base/tests/test_osv_expression.yml index 73bbdfae3eb..1ed07c4a8ff 100644 --- a/openerp/addons/base/tests/test_osv_expression.yml +++ b/openerp/addons/base/tests/test_osv_expression.yml @@ -51,6 +51,42 @@ !python {model: res.partner.category }: | ids = self.search(cr, uid, [('id', 'child_of',[ref('categ_1')])]) assert len(ids) == 1, ids +- + Test hierarchical search in M2M with parent ID (list of ids) +- + !python {model: res.partner.category }: | + ids = self.search(cr, uid, [('id', 'parent_of',[ref('categ_1')])]) + assert len(ids) == 3, ids +- + Test hierarchical search in M2M with parent ID (single id) +- + !python {model: res.partner.category }: | + ids = self.search(cr, uid, [('id', 'parent_of',ref('categ_1'))]) + assert len(ids) == 3, ids +- + Test hierarchical search in M2M with parent IDs +- + !python {model: res.partner.category }: | + ids = self.search(cr, uid, [('id', 'parent_of',[ref('categ_root'), ref('categ_0')])]) + assert len(ids) == 2, ids +- + Test hierarchical search in M2M with parent IDs +- + !python {model: res.partner.category }: | + ids = self.search(cr, uid, [('id', 'parent_of',[ref('categ_0')])]) + assert len(ids) == 2, ids +- + Test hierarchical search in M2M with parent IDs +- + !python {model: res.partner.category }: | + ids = self.search(cr, uid, [('id', 'parent_of',[ref('categ_1')])]) + assert len(ids) == 3, ids +- + Test hierarchical search in M2M with parent IDs +- + !python {model: res.partner.category }: | + ids = self.search(cr, uid, [('id', 'parent_of',[ref('categ_root')])]) + assert len(ids) == 1, ids - Testing that some domain expressions work - @@ -405,6 +441,23 @@ assert res_2 == expected assert res_3 == expected assert res_4 == expected + + # parent_of x returns x and its parents (direct or not). + company = self.browse(cr, uid, ref('ymltest_company4')) + expected = [ref('ymltest_company4'), ref('ymltest_company3')] + expected.sort() + res_1 = self.search(cr, uid, [('id', 'parent_of', [ref('ymltest_company4')])]) + res_1.sort() + res_2 = self.search(cr, uid, [('id', 'parent_of', ref('ymltest_company4'))]) + res_2.sort() + res_3 = self.search(cr, uid, [('id', 'parent_of', [company.name])]) + res_3.sort() + res_4 = self.search(cr, uid, [('id', 'parent_of', company.name)]) + res_4.sort() + assert res_1 == expected + assert res_2 == expected + assert res_3 == expected + assert res_4 == expected - Equivalent queries, one2many. - diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 1e9541b1abe..b0ef9f0046a 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -141,7 +141,7 @@ DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR) # operators are also used. In this case its right operand has the form (subselect, params). TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', - 'child_of') + 'child_of', 'parent_of') # A subset of the above operators, with a 'negative' semantic. When the # expressions 'in NEGATIVE_TERM_OPERATORS' or 'not in NEGATIVE_TERM_OPERATORS' are used in the code @@ -725,6 +725,34 @@ class expression(object): return ids + recursive_children(ids2, model, parent_field) return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] + def parent_of_domain(left, ids, left_model, parent=None, prefix='', context=None): + """ 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 + (when available), or as an expanded [(left,in,parent_ids)] """ + if left_model._parent_store and (not left_model.pool._init): + doms = [] + for node in left_model.browse(cr, uid, ids, context=context): + if doms: + doms.insert(0, OR_OPERATOR) + doms += [AND_OPERATOR, ('parent_right', '>', node.parent_left), ('parent_left', '<=', node.parent_left)] + if prefix: + return [(left, 'in', left_model.search(cr, uid, doms, context=context))] + return doms + else: + def get_parent_ids(record, parent_field): + ids = set([record.id]) + while record[parent_field]: + record = record[parent_field] + ids.add(record.id) + return ids + parent_ids = set() + for node in left_model.browse(cr, uid, ids, context=context): + parent_ids |= get_parent_ids(node, parent or left_model._parent_name) + return [(left, 'in', list(parent_ids))] + + HIERARCHY_FUNCS = {'child_of': child_of_domain, + 'parent_of': parent_of_domain} + def pop(): """ Pop a leaf to process. """ return self.stack.pop() @@ -791,9 +819,9 @@ class expression(object): leaf.add_join_context(next_model, model._inherits[next_model._name], 'id', model._inherits[next_model._name]) push(leaf) - elif left == 'id' and operator == 'child_of': + elif left == 'id' and operator in HIERARCHY_FUNCS: ids2 = to_ids(right, model, context) - dom = child_of_domain(left, ids2, model) + dom = HIERARCHY_FUNCS[operator](left, ids2, model) for dom_leaf in reversed(dom): new_leaf = create_substitution_leaf(leaf, dom_leaf, model) push(new_leaf) @@ -906,12 +934,12 @@ class expression(object): # ------------------------------------------------- # Applying recursivity on field(one2many) - elif column._type == 'one2many' and operator == 'child_of': + elif column._type == 'one2many' and operator in HIERARCHY_FUNCS: ids2 = to_ids(right, comodel, context) if column._obj != model._name: - dom = child_of_domain(left, ids2, comodel, prefix=column._obj) + dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=column._obj) else: - dom = child_of_domain('id', ids2, model, parent=left) + dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left) for dom_leaf in reversed(dom): push(create_substitution_leaf(leaf, dom_leaf, model)) @@ -952,14 +980,15 @@ class expression(object): elif column._type == 'many2many': rel_table, rel_id1, rel_id2 = column._sql_names(model) - if operator == 'child_of': + + if operator in HIERARCHY_FUNCS: def _rec_convert(ids): if comodel == model: return ids return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) ids2 = to_ids(right, comodel, context) - dom = child_of_domain('id', ids2, comodel) + dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel) ids2 = comodel.search(cr, uid, dom, context=context) push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model)) else: @@ -992,12 +1021,12 @@ class expression(object): push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), model)) elif column._type == 'many2one': - if operator == 'child_of': + if operator in HIERARCHY_FUNCS: ids2 = to_ids(right, comodel, context) if column._obj != model._name: - dom = child_of_domain(left, ids2, comodel, prefix=column._obj) + dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=column._obj) else: - dom = child_of_domain('id', ids2, model, parent=left) + dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left) for dom_leaf in reversed(dom): push(create_substitution_leaf(leaf, dom_leaf, model)) else: