diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 5286e1c5657..2bfd8ade1c9 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -381,7 +381,42 @@ class test_expression(common.TransactionCase): "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect parameter") # ---------------------------------------- - # Test3: result-based tests + # Test3: domain attribute on one2many fields + # ---------------------------------------- + + partner_child_ids_col._auto_join = True + partner_bank_ids_col._auto_join = True + partner_child_ids_col._domain = lambda self: ['!', ('name', '=', self._name)] + partner_bank_ids_col._domain = [('acc_number', 'like', '1')] + # Do: 2 cascaded one2many with _auto_join, test final leaf is an id + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + for query in self.query_list: + print query + print partner_ids + print '--------------------' + # Test result: at least one of our added data + self.assertTrue(set([p_a]).issubset(set(partner_ids)), + "_auto_join on one2many with domains incorrect result") + self.assertTrue(set([p_ab, p_ba]) not in set(partner_ids), + "_auto_join on one2many with domains incorrect result") + # Test produced queries that domains effectively present + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1], + "_auto_join on one2many with domains incorrect result") + # TDE TODO: check first domain has a correct table name + self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1], + "_auto_join on one2many with domains incorrect result") + + partner_child_ids_col._domain = lambda self: [('name', '=', '__%s' % self._name)] + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + # Test result: no one + self.assertFalse(partner_ids, + "_auto_join on one2many with domains incorrect result") + + # ---------------------------------------- + # Test4: result-based tests # ---------------------------------------- partner_bank_ids_col._auto_join = False @@ -389,6 +424,8 @@ class test_expression(common.TransactionCase): partner_state_id_col._auto_join = False partner_parent_id_col._auto_join = False state_country_id_col._auto_join = False + partner_child_ids_col._domain = [] + partner_bank_ids_col._domain = [] # Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join self._reinit_mock() @@ -419,12 +456,12 @@ class test_expression(common.TransactionCase): self.assertEqual(len(self.query_list), 1, "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") - # # Remove mocks and modifications - # partner_bank_ids_col._auto_join = False - # partner_child_ids_col._auto_join = False - # partner_state_id_col._auto_join = False - # state_country_id_col._auto_join = False - # BaseModel._where_calc = self._base_model_where_calc + # Remove mocks and modifications + partner_bank_ids_col._auto_join = False + partner_child_ids_col._auto_join = False + partner_state_id_col._auto_join = False + state_country_id_col._auto_join = False + BaseModel._where_calc = self._base_model_where_calc if __name__ == '__main__': unittest2.main() diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 2474e6665cd..9078cf5c42c 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -415,32 +415,32 @@ class ExtendedLeaf(object): for item in self.context_stack: self._tables.append(item[0]) self._tables.append(table) - # if leaf.is_operator: - # self.elements = self.leaf, None, None - # elif leaf.is_true_leaf() or leaf.is_false_leaf(): - # # because we consider left as a string - # self.elements = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2]) - # else: - # self.elements = leaf.leaf - # self.field_path = self.elements[0].split('.', 1) - # self.field = self.table._columns.get(self.field_path[0]) - # if self.field and self.field._obj: - # relational_table = working_table.pool.get(field._obj) - # else: - # relational_table = None + if self.is_operator(): + self.elements = self.leaf, None, None + elif self.is_true_leaf() or self.is_false_leaf(): + # because we consider left as a string + self.elements = ('%s' % leaf[0], leaf[1], leaf[2]) + else: + self.elements = leaf + self.field_path = self.elements[0].split('.', 1) + self.field = self.table._columns.get(self.field_path[0]) + if self.field and self.field._obj: + self.relational_table = self.table.pool.get(self.field._obj) + else: + self.relational_table = None # check validity self.check_leaf() def __str__(self): return '' % (str(self.leaf), self.table._table, ','.join(self._get_context_debug())) - def create_substitution_leaf(self, new_leaf, new_table=None): - if new_table is None: - new_table = self.table - return ExtendedLeaf(new_leaf, new_table, self.context_stack) + # def create_substitution_leaf(self, new_leaf, new_table=None): + # if new_table is None: + # new_table = self.table + # return ExtendedLeaf(new_leaf, new_table, self.context_stack) - def create_sibling_leaf(self, new_leaf): - pass + # def create_sibling_leaf(self, new_leaf): + # pass # -------------------------------------------------- # Join / Context manipulation @@ -464,7 +464,7 @@ class ExtendedLeaf(object): # i.e.: many2one: res_country_state as res_partner__state_id # - join condition use aliases # i.e.: inherits: res_users.partner_id = res_users__partner_id.id - # i.e.: one2many: res_partner.id = res_partner__bank_ids.partner_id + # i.e.: one2many: res_partner.id = res_partner__bank_ids.parr_id # i.e.: many2one: res_partner.state_id = res_partner__state_id.id # Variables explanation: # - src_table: working table before the join @@ -646,12 +646,9 @@ class expression(object): return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] # ---------------------------------------- - # Internal structure + # Leafs management # ---------------------------------------- - def _format_table_name(self, table_name): - return '"%s"' % (table_name) - def get_tables(self): """ Returns the list of tables for SQL queries, like select from ... """ tables = [] @@ -664,6 +661,16 @@ class expression(object): tables.append(table_name) return tables + def create_substitution_leaf(self, leaf, new_elements, new_table=None): + if new_table is None: + new_table = leaf.table + new_context_stack = [tuple(context) for context in leaf.context_stack] + new_leaf = ExtendedLeaf(new_elements, new_table, context_stack=new_context_stack) + return new_leaf + + def create_sibling_leaf(self, leaf, new_elements): + pass + # ---------------------------------------- # Parsing # ---------------------------------------- @@ -697,6 +704,11 @@ class expression(object): # Get working variables working_table = leaf.table + # left, operator, right = leaf.elements + # field_path = leaf.field_path + # field = leaf.field + # relational_table = leaf.relational_table + if leaf.is_operator(): left, operator, right = leaf.leaf, None, None elif leaf.is_true_leaf() or leaf.is_false_leaf(): @@ -748,30 +760,43 @@ class expression(object): elif not field and left == 'id' and operator == 'child_of': ids2 = self.to_ids(cr, uid, right, working_table, context) dom = self.child_of_domain(cr, uid, left, ids2, working_table) - leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] elif not field and field_path[0] in MAGIC_COLUMNS: results_to_stack.append(leaf) elif not field: - raise ValueError("Invalid field %r in leaf %r" % (left, leaf)) + raise ValueError("Invalid field %r in leaf %r" % (left, str(leaf))) # ---------------------------------------- # PATH SPOTTED - # -> XX + # -> many2one or one2many with _auto_join: + # - add a join, then jump into linked field: field.remaining on + # src_table is replaced by remaining on dst_table, and set for re-evaluation + # - if a domain is defined on the field, add it into evaluation + # on the relational table + # -> many2one, many2many, one2many: replace by an equivalent computed + # domain, given by recursively searching on the remaining of the path # -> note: hack about fields.property should not be necessary anymore # as after transforming the field, it will go through this loop once again # ---------------------------------------- elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join: - # res_partner.parent_id = res_partner.id + # res_partner.state_id = res_partner__state_id.id leaf.add_join_context(field_path[0], field_path[0], relational_table, 'id') - leafs_to_stack.append(leaf.create_substitution_leaf((field_path[1], operator, right), relational_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join: - # res_partner.id = res_partner.parent_id + # res_partner.id = res_partner__bank_ids.partner_id leaf.add_join_context(field_path[0], 'id', relational_table, field._fields_id) - leafs_to_stack.append(leaf.create_substitution_leaf((field_path[1], operator, right), relational_table)) + domain = field._domain(working_table) if callable(field._domain) else field._domain + if domain: + domain = normalize_domain(domain) + leafs_to_stack.append(self.create_substitution_leaf(leaf, AND_OPERATOR, relational_table)) + for elem in domain: + leafs_to_stack.append(self.create_substitution_leaf(leaf, elem, relational_table)) + print '--> appending %s' % str(leafs_to_stack[-1]) + leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) elif len(field_path) > 1 and field._auto_join: assert False, \ @@ -817,10 +842,10 @@ class expression(object): else: # we assume that the expression is valid # we create a dummy leaf for forcing the parsing of the resulting expression - leafs_to_stack.append(leaf.create_substitution_leaf(AND_OPERATOR, working_table)) - leafs_to_stack.append(leaf.create_substitution_leaf(TRUE_LEAF, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, AND_OPERATOR, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, TRUE_LEAF, working_table)) for domain_element in fct_domain: - leafs_to_stack.append(leaf.create_substitution_leaf(domain_element, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, domain_element, working_table)) # Applying recursivity on field(one2many) elif field._type == 'one2many' and operator == 'child_of': @@ -829,7 +854,7 @@ class expression(object): dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) else: dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) - leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] elif field._type == 'one2many': call_null = True @@ -848,17 +873,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null = False - leafs_to_stack.append(leaf.create_substitution_leaf(FALSE_LEAF, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, FALSE_LEAF, working_table)) else: ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) if ids2: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', o2m_op, ids2), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_table)) if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) elif field._type == 'many2many': rel_table, rel_id1, rel_id2 = field._sql_names(working_table) @@ -872,7 +897,7 @@ class expression(object): ids2 = self.to_ids(cr, uid, right, relational_table, context) dom = self.child_of_domain(cr, uid, 'id', ids2, relational_table) ids2 = relational_table.search(cr, uid, dom, context=context) - leafs_to_stack.append(leaf.create_substitution_leaf(('id', 'in', _rec_convert(ids2)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_table)) else: call_null_m2m = True if right is not False: @@ -889,17 +914,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null_m2m = False - leafs_to_stack.append(leaf.create_substitution_leaf(FALSE_LEAF, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, FALSE_LEAF, working_table)) else: operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) elif field._type == 'many2one': if operator == 'child_of': @@ -908,7 +933,7 @@ class expression(object): dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) else: dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) - leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] else: def _get_expression(relational_table, cr, uid, left, right, operator, context=None): if context is None: @@ -932,7 +957,7 @@ class expression(object): # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - leafs_to_stack.append(leaf.create_substitution_leaf(_get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, _get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() results_to_stack.append(leaf) @@ -947,7 +972,7 @@ class expression(object): elif operator in ('<', '<='): right += ' 23:59:59' - leafs_to_stack.append(leaf.create_substitution_leaf((left, operator, right), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, (left, operator, right), working_table)) elif field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') @@ -982,7 +1007,7 @@ class expression(object): right, right, ] - leafs_to_stack.append(leaf.create_substitution_leaf(('id', 'inselect', (subselect, params)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', 'inselect', (subselect, params)), working_table)) else: results_to_stack.append(leaf)