diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py index b289d44bbaf..47923ead732 100644 --- a/openerp/addons/base/tests/test_base.py +++ b/openerp/addons/base/tests/test_base.py @@ -464,6 +464,17 @@ class test_translation(common.TransactionCase): fr_context_cat = self.res_category.browse(cr, uid, self.new_fr_cat_id, context={'lang':'fr_FR'}) self.assertEqual(fr_context_cat.name, 'Clients (copie)', "Did not used default value for translated value") + def test_104_orderby_translated_field(self): + """ Test search ordered by a translated field. """ + # create a category with a French translation + category = self.env['res.partner.category'].create({'name': 'Padawans'}) + category_fr = category.with_context(lang='fr_FR') + category_fr.write({'name': 'Apprentis'}) + # search for categories, and sort them by (translated) name + categories = category_fr.search([('id', 'in', [self.cat_id, category.id])], order='name') + self.assertEqual(categories.ids, [category.id, self.cat_id], + "Search ordered by translated name should return Padawans (Apprentis) before Customers (Clients)") + test_state = None #: Stores state information across multiple test classes def setUpModule(): diff --git a/openerp/models.py b/openerp/models.py index 130525f0943..bd213f8e55c 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -2183,7 +2183,11 @@ class BaseModel(object): implicit=True, ) model, alias = parent_model, parent_alias - return '"%s"."%s"' % (alias, field) + # handle the case where the field is translated + if model._columns[field].translate: + return model._generate_translated_field(alias, field, query) + else: + return '"%s"."%s"' % (alias, field) def _parent_store_compute(self, cr): if not self._parent_store: @@ -4525,6 +4529,27 @@ class BaseModel(object): apply_rule(rule_where_clause, rule_where_clause_params, rule_tables, parent_model=inherited_model) + @api.model + def _generate_translated_field(self, table_alias, field, query): + """ + Add possibly missing JOIN with translations table to ``query`` and + generate the expression for the translated field. + + :return: the qualified field name (or expression) to use for ``field`` + """ + lang = self._context.get('lang') + if lang and lang != 'en_US': + alias, alias_statement = query.add_join( + (table_alias, 'ir_translation', 'id', 'res_id', field), + implicit=False, + outer=True, + extra='"{rhs}"."name" = %s AND "{rhs}"."lang" = %s AND "{rhs}"."value" != %s', + extra_params=["%s,%s" % (self._name, field), lang, ""], + ) + return 'COALESCE("%s"."%s", "%s"."%s")' % (alias, 'value', table_alias, field) + else: + return '"%s"."%s"' % (table_alias, field) + @api.model def _generate_m2o_order_by(self, order_field, query): """ @@ -4593,7 +4618,10 @@ class BaseModel(object): elif order_field in self._columns: order_column = self._columns[order_field] if order_column._classic_read: - inner_clause = '"%s"."%s"' % (self._table, order_field) + if order_column.translate: + inner_clause = self._generate_translated_field(self._table, order_field, query) + else: + inner_clause = '"%s"."%s"' % (self._table, order_field) elif order_column._type == 'many2one': inner_clause = self._generate_m2o_order_by(order_field, query) else: diff --git a/openerp/osv/query.py b/openerp/osv/query.py index 8c52aa8b753..3fbdc476d4e 100644 --- a/openerp/osv/query.py +++ b/openerp/osv/query.py @@ -21,7 +21,7 @@ class Query(object): - etc. """ - def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None): + def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None, extras=None): # holds the list of tables joined using default JOIN. # the table names are stored double-quoted (backwards compatibility) @@ -49,6 +49,21 @@ class Query(object): # LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col") self.joins = joins or {} + # holds extra conditions for table joins that should not be in the where + # clause but in the join condition itself. The dict is used as follows: + # + # self.extras = { + # ('table_a', ('table_b', 'table_a_col1', 'table_b_col', 'LEFT JOIN')): + # ('"table_b"."table_b_col3" = %s', [42]) + # } + # + # which should lead to the following SQL: + # + # SELECT ... FROM "table_a" + # LEFT JOIN "table_b" ON ("table_a"."table_a_col1" = "table_b"."table_b_col" AND "table_b"."table_b_col3" = 42) + # ... + self.extras = extras or {} + def _get_table_aliases(self): from openerp.osv.expression import get_alias_from_query return [get_alias_from_query(from_statement)[1] for from_statement in self.tables] @@ -61,7 +76,7 @@ class Query(object): mapping[statement] = table return mapping - def add_join(self, connection, implicit=True, outer=False): + def add_join(self, connection, implicit=True, outer=False, extra=None, extra_params=[]): """ Join a destination table to the current table. :param implicit: False if the join is an explicit join. This allows @@ -69,7 +84,7 @@ class Query(object): OpenERP 7.0. It therefore adds the JOIN specified in ``connection`` If True, the join is done implicitely, by adding the table alias in the from clause and the join condition in the where clause - of the query. Implicit joins do not handle outer parameter. + of the query. Implicit joins do not handle outer, extra, extra_params parameters. :param connection: a tuple ``(lhs, table, lhs_col, col, link)``. The join corresponds to the SQL equivalent of:: @@ -84,6 +99,14 @@ class Query(object): columns so it would not be correct (e.g. for ``_inherits`` or when a domain criterion explicitly adds filtering) + + :param extra: A string with the extra join condition (SQL), or None. + This is used to provide an additional condition to the join + clause that cannot be added in the where clause (e.g., for LEFT + JOIN concerns). The condition string should refer to the table + aliases as "{lhs}" and "{rhs}". + + :param extra_params: a list of parameters for the `extra` condition. """ from openerp.osv.expression import generate_table_alias (lhs, table, lhs_col, col, link) = connection @@ -107,32 +130,43 @@ class Query(object): else: # add JOIN self.tables.append(alias_statement) - self.joins.setdefault(lhs, []).append((alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) + join_tuple = (alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN') + self.joins.setdefault(lhs, []).append(join_tuple) + if extra: + extra = extra.format(lhs=lhs, rhs=alias) + self.extras[(lhs, join_tuple)] = (extra, extra_params) return alias, alias_statement def get_sql(self): """ Returns (query_from, query_where, query_params). """ from openerp.osv.expression import get_alias_from_query - query_from = '' tables_to_process = list(self.tables) alias_mapping = self._get_alias_mapping() + from_clause = [] + from_params = [] - def add_joins_for_table(table, query_from): - for (dest_table, lhs_col, col, join) in self.joins.get(table, []): - tables_to_process.remove(alias_mapping[dest_table]) - query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \ - (join, alias_mapping[dest_table], table, lhs_col, dest_table, col) - query_from = add_joins_for_table(dest_table, query_from) - return query_from + def add_joins_for_table(lhs): + for (rhs, lhs_col, rhs_col, join) in self.joins.get(lhs, []): + tables_to_process.remove(alias_mapping[rhs]) + from_clause.append(' %s %s ON ("%s"."%s" = "%s"."%s"' % \ + (join, alias_mapping[rhs], lhs, lhs_col, rhs, rhs_col)) + extra = self.extras.get((lhs, (rhs, lhs_col, rhs_col, join))) + if extra: + from_clause.append(' AND ') + from_clause.append(extra[0]) + from_params.extend(extra[1]) + from_clause.append(')') + add_joins_for_table(rhs) - for table in tables_to_process: - query_from += table + for pos, table in enumerate(tables_to_process): + if pos > 0: + from_clause.append(',') + from_clause.append(table) table_alias = get_alias_from_query(table)[1] if table_alias in self.joins: - query_from = add_joins_for_table(table_alias, query_from) - query_from += ',' - query_from = query_from[:-1] # drop last comma - return query_from, " AND ".join(self.where_clause), self.where_clause_params + add_joins_for_table(table_alias) + + return "".join(from_clause), " AND ".join(self.where_clause), from_params + self.where_clause_params def __str__(self): return '' % self.get_sql()