Exemplo n.º 1
0
    def _compute_application_count(self):
        self.flush(fnames=['email_from'])
        # Filter and gather emails at the same time
        applicants = self.env['hr.applicant']
        mails = set()
        for applicant in self:
            if applicant.email_from:
                applicants |= applicant
                mails.add(applicant.email_from.lower())
        # Done via SQL since read_group does not support grouping by lowercase field
        if mails:
            query = Query(self.env.cr, self._table, self._table_query)
            query.add_where('LOWER("hr_applicant".email_from) in %s', [tuple(mails)])
            self._apply_ir_rules(query)
            from_clause, where_clause, where_clause_params = query.get_sql()
            query_str = """
            SELECT LOWER("%(table)s".email_from) as l_email_from,
                COUNT("%(table)s".id) as count
            FROM %(from)s
            %(where)s
        GROUP BY l_email_from
            """ % {
                'table': self._table,
                'from': from_clause,
                'where': ('WHERE %s' % where_clause) if where_clause else '',
            }
            self.env.cr.execute(query_str, where_clause_params)

            application_data_mapped = dict((data['l_email_from'], data['count']) for data in self.env.cr.dictfetchall())
        else:
            application_data_mapped = dict()
        for applicant in applicants:
            applicant.application_count = application_data_mapped.get(applicant.email_from.lower(), 1)
        (self - applicants).application_count = False
Exemplo n.º 2
0
    def test_mixed_query_chained_explicit_implicit_joins(self):
        query = Query(None, 'product_product')
        query.add_table('product_template')
        query.add_where("product_product.template_id = product_template.id")
        # add inner join
        alias = query.join("product_template", "categ_id", "product_category",
                           "id", "categ_id")
        self.assertEqual(alias, 'product_template__categ_id')
        # add CHAINED left join
        alias = query.left_join("product_template__categ_id", "user_id",
                                "res_user", "id", "user_id")
        self.assertEqual(alias, 'product_template__categ_id__user_id')
        # additional implicit join
        query.add_table('account.account')
        query.add_where(
            "product_category.expense_account_id = account_account.id")

        from_clause, where_clause, where_params = query.get_sql()
        self.assertEqual(
            from_clause,
            '"product_product", "product_template", "account.account" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")'
        )
        self.assertEqual(
            where_clause,
            "product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id"
        )
Exemplo n.º 3
0
    def test_basic_query(self):
        query = Query(None, 'product_product')
        query.add_table('product_template')
        query.add_where("product_product.template_id = product_template.id")
        # add inner join
        alias = query.join("product_template", "categ_id", "product_category",
                           "id", "categ_id")
        self.assertEqual(alias, 'product_template__categ_id')
        # add left join
        alias = query.left_join("product_product", "user_id", "res_user", "id",
                                "user_id")
        self.assertEqual(alias, 'product_product__user_id')

        from_clause, where_clause, where_params = query.get_sql()
        self.assertEqual(
            from_clause,
            '"product_product", "product_template" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id")'
        )
        self.assertEqual(where_clause,
                         "product_product.template_id = product_template.id")
Exemplo n.º 4
0
 def _compute_application_count(self):
     self.flush_model(['email_from'])
     applicants = self.env['hr.applicant']
     for applicant in self:
         if applicant.email_from or applicant.partner_phone or applicant.partner_mobile:
             applicants |= applicant
     # Done via SQL since read_group does not support grouping by lowercase field
     if applicants.ids:
         query = Query(self.env.cr, self._table, self._table_query)
         query.add_where('hr_applicant.id in %s', [tuple(applicants.ids)])
         # Count into the companies that are selected from the multi-company widget
         company_ids = self.env.context.get('allowed_company_ids')
         if company_ids:
             query.add_where('other.company_id in %s', [tuple(company_ids)])
         self._apply_ir_rules(query)
         from_clause, where_clause, where_clause_params = query.get_sql()
         # In case the applicant phone or mobile is configured in wrong field
         query_str = """
         SELECT hr_applicant.id as appl_id,
             COUNT(other.id) as count
           FROM hr_applicant
           JOIN hr_applicant other ON LOWER(other.email_from) = LOWER(hr_applicant.email_from)
             OR other.partner_phone = hr_applicant.partner_phone OR other.partner_phone = hr_applicant.partner_mobile
             OR other.partner_mobile = hr_applicant.partner_mobile OR other.partner_mobile = hr_applicant.partner_phone
         %(where)s
     GROUP BY hr_applicant.id
         """ % {
             'where': ('WHERE %s' % where_clause) if where_clause else '',
         }
         self.env.cr.execute(query_str, where_clause_params)
         application_data_mapped = dict(
             (data['appl_id'], data['count'])
             for data in self.env.cr.dictfetchall())
     else:
         application_data_mapped = dict()
     for applicant in applicants:
         applicant.application_count = application_data_mapped.get(
             applicant.id, 1) - 1
     (self - applicants).application_count = False
Exemplo n.º 5
0
class expression(object):
    """ Parse a domain expression
        Use a real polish notation
        Leafs are still in a ('foo', '=', 'bar') format
        For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
    """

    def __init__(self, domain, model, alias=None, query=None):
        """ Initialize expression object and automatically parse the expression
            right after initialization.

            :param domain: expression (using domain ('foo', '=', 'bar') format)
            :param model: root model
            :param alias: alias for the model table if query is provided
            :param query: optional query object holding the final result

            :attr root_model: base model for the query
            :attr expression: the domain to parse, normalized and prepared
            :attr result: the result of the parsing, as a pair (query, params)
            :attr query: Query object holding the final result
        """
        self._unaccent = get_unaccent_wrapper(model._cr)
        self.root_model = model
        self.root_alias = alias or model._table

        # normalize and prepare the expression for parsing
        self.expression = distribute_not(normalize_domain(domain))

        # this object handles all the joins
        self.query = Query(model.env.cr, model._table, model._table_query) if query is None else query

        # parse the domain expression
        self.parse()

    # ----------------------------------------
    # Leafs management
    # ----------------------------------------

    def get_tables(self):
        warnings.warn("deprecated expression.get_tables(), use expression.query instead",
                      DeprecationWarning)
        return self.query.tables

    # ----------------------------------------
    # Parsing
    # ----------------------------------------

    def parse(self):
        """ Transform the leaves of the expression

            The principle is to pop elements from a leaf stack one at a time.
            Each leaf is processed. The processing is a if/elif list of various
            cases that appear in the leafs (many2one, function fields, ...).
            Three things can happen as a processing result:
            - the leaf is a logic operator, and updates the result stack
              accordingly;
            - the leaf has been modified and/or new leafs have to be introduced
              in the expression; they are pushed into the leaf stack, to be
              processed right after;
            - the leaf is converted to SQL and added to the result stack

            Here is a suggested execution:

                step                stack               result_stack

                                    ['&', A, B]         []
                substitute B        ['&', A, B1]        []
                convert B1 in SQL   ['&', A]            ["B1"]
                substitute A        ['&', '|', A1, A2]  ["B1"]
                convert A2 in SQL   ['&', '|', A1]      ["B1", "A2"]
                convert A1 in SQL   ['&', '|']          ["B1", "A2", "A1"]
                apply operator OR   ['&']               ["B1", "A1 or A2"]
                apply operator AND  []                  ["(A1 or A2) and B1"]

            Some internal var explanation:
                :var list path: left operand seen as a sequence of field names
                    ("foo.bar" -> ["foo", "bar"])
                :var obj model: model object, model containing the field
                    (the name provided in the left operand)
                :var obj field: the field corresponding to `path[0]`
                :var obj column: the column corresponding to `path[0]`
                :var obj comodel: relational model of field (field.comodel)
                    (res_partner.bank_ids -> res.partner.bank)
        """
        cr, uid, context, su = self.root_model.env.args

        def to_ids(value, comodel, leaf):
            """ Normalize a single id or name, or a list of those, into a list of ids
                :param {int,long,basestring,list,tuple} value:
                    if int, long -> return [value]
                    if basestring, convert it into a list of basestrings, then
                    if list of basestring ->
                        perform a name_search on comodel for each name
                        return the list of related ids
            """
            names = []
            if isinstance(value, str):
                names = [value]
            elif value and isinstance(value, (tuple, list)) and all(isinstance(item, str) for item in value):
                names = value
            elif isinstance(value, int):
                if not value:
                    # given this nonsensical domain, it is generally cheaper to
                    # interpret False as [], so that "X child_of False" will
                    # match nothing
                    _logger.warning("Unexpected domain [%s], interpreted as False", leaf)
                    return []
                return [value]
            if names:
                return list({
                    rid
                    for name in names
                    for rid in comodel._name_search(name, [], 'ilike', limit=None)
                })
            return list(value)

        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_path tree lookup field
                (when available), or as an expanded [(left,in,child_ids)] """
            if not ids:
                return [FALSE_LEAF]
            if left_model._parent_store:
                domain = OR([
                    [('parent_path', '=like', rec.parent_path + '%')]
                    for rec in left_model.browse(ids)
                ])
            else:
                # recursively retrieve all children nodes with sudo(); the
                # filtering of forbidden records is done by the rest of the
                # domain
                parent_name = parent or left_model._parent_name
                child_ids = set()
                records = left_model.sudo().browse(ids)
                while records:
                    child_ids.update(records._ids)
                    records = records.search([(parent_name, 'in', records.ids)], order='id')
                domain = [('id', 'in', list(child_ids))]
            if prefix:
                return [(left, 'in', left_model._search(domain, order='id'))]
            return domain

        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_path tree lookup field
                (when available), or as an expanded [(left,in,parent_ids)] """
            if not ids:
                return [FALSE_LEAF]
            if left_model._parent_store:
                parent_ids = [
                    int(label)
                    for rec in left_model.browse(ids)
                    for label in rec.parent_path.split('/')[:-1]
                ]
                domain = [('id', 'in', parent_ids)]
            else:
                # recursively retrieve all parent nodes with sudo() to avoid
                # access rights errors; the filtering of forbidden records is
                # done by the rest of the domain
                parent_name = parent or left_model._parent_name
                parent_ids = set()
                records = left_model.sudo().browse(ids)
                while records:
                    parent_ids.update(records._ids)
                    records = records[parent_name]
                domain = [('id', 'in', list(parent_ids))]
            if prefix:
                return [(left, 'in', left_model._search(domain, order='id'))]
            return domain

        HIERARCHY_FUNCS = {'child_of': child_of_domain,
                           'parent_of': parent_of_domain}

        def pop():
            """ Pop a leaf to process. """
            return stack.pop()

        def push(leaf, model, alias, internal=False):
            """ Push a leaf to be processed right after. """
            leaf = normalize_leaf(leaf)
            check_leaf(leaf, internal)
            stack.append((leaf, model, alias))

        def pop_result():
            return result_stack.pop()

        def push_result(query, params):
            result_stack.append((query, params))

        # process domain from right to left; stack contains domain leaves, in
        # the form: (leaf, corresponding model, corresponding table alias)
        stack = []
        for leaf in self.expression:
            push(leaf, self.root_model, self.root_alias)

        # stack of SQL expressions in the form: (expr, params)
        result_stack = []

        while stack:
            # Get the next leaf to process
            leaf, model, alias = pop()

            # ----------------------------------------
            # SIMPLE CASE
            # 1. leaf is an operator
            # 2. leaf is a true/false leaf
            # -> convert and add directly to result
            # ----------------------------------------

            if is_operator(leaf):
                if leaf == NOT_OPERATOR:
                    expr, params = pop_result()
                    push_result('(NOT (%s))' % expr, params)
                else:
                    ops = {AND_OPERATOR: '(%s AND %s)', OR_OPERATOR: '(%s OR %s)'}
                    lhs, lhs_params = pop_result()
                    rhs, rhs_params = pop_result()
                    push_result(ops[leaf] % (lhs, rhs), lhs_params + rhs_params)
                continue

            if is_boolean(leaf):
                expr, params = self.__leaf_to_sql(leaf, model, alias)
                push_result(expr, params)
                continue

            # Get working variables
            left, operator, right = leaf
            path = left.split('.', 1)

            field = model._fields.get(path[0])
            comodel = model.env.get(getattr(field, 'comodel_name', None))

            # ----------------------------------------
            # FIELD NOT FOUND
            # -> from inherits'd fields -> work on the related model, and add
            #    a join condition
            # -> ('id', 'child_of', '..') -> use a 'to_ids'
            # -> but is one on the _log_access special fields, add directly to
            #    result
            #    TODO: make these fields explicitly available in self.columns instead!
            # -> else: crash
            # ----------------------------------------

            if not field:
                raise ValueError("Invalid field %s.%s in leaf %s" % (model._name, path[0], str(leaf)))

            elif field.inherited:
                parent_model = model.env[field.related_field.model_name]
                parent_fname = model._inherits[parent_model._name]
                parent_alias = self.query.left_join(
                    alias, parent_fname, parent_model._table, 'id', parent_fname,
                )
                push(leaf, parent_model, parent_alias)

            elif left == 'id' and operator in HIERARCHY_FUNCS:
                ids2 = to_ids(right, model, leaf)
                dom = HIERARCHY_FUNCS[operator](left, ids2, model)
                for dom_leaf in dom:
                    push(dom_leaf, model, alias)

            # ----------------------------------------
            # PATH SPOTTED
            # -> many2one or one2many with _auto_join:
            #    - add a join, then jump into linked column: column.remaining on
            #      src_table is replaced by remaining on dst_table, and set for re-evaluation
            #    - if a domain is defined on the column, 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 columns.property should not be necessary anymore
            #    as after transforming the column, it will go through this loop once again
            # ----------------------------------------

            elif len(path) > 1 and field.store and field.type == 'many2one' and field.auto_join:
                # res_partner.state_id = res_partner__state_id.id
                coalias = self.query.left_join(
                    alias, path[0], comodel._table, 'id', path[0],
                )
                push((path[1], operator, right), comodel, coalias)

            elif len(path) > 1 and field.store and field.type == 'one2many' and field.auto_join:
                # use a subquery bypassing access rules and business logic
                domain = [(path[1], operator, right)] + field.get_domain_list(model)
                query = comodel.with_context(**field.context)._where_calc(domain)
                subquery, subparams = query.select('"%s"."%s"' % (comodel._table, field.inverse_name))
                push(('id', 'inselect', (subquery, subparams)), model, alias, internal=True)

            elif len(path) > 1 and field.store and field.auto_join:
                raise NotImplementedError('auto_join attribute not supported on field %s' % field)

            elif len(path) > 1 and field.store and field.type == 'many2one':
                right_ids = comodel.with_context(active_test=False)._search([(path[1], operator, right)], order='id')
                push((path[0], 'in', right_ids), model, alias)

            # Making search easier when there is a left operand as one2many or many2many
            elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'):
                right_ids = comodel.with_context(**field.context)._search([(path[1], operator, right)], order='id')
                push((path[0], 'in', right_ids), model, alias)

            elif not field.store:
                # Non-stored field should provide an implementation of search.
                if not field.search:
                    # field does not support search!
                    _logger.error("Non-stored field %s cannot be searched.", field)
                    if _logger.isEnabledFor(logging.DEBUG):
                        _logger.debug(''.join(traceback.format_stack()))
                    # Ignore it: generate a dummy leaf.
                    domain = []
                else:
                    # Let the field generate a domain.
                    if len(path) > 1:
                        right = comodel._search([(path[1], operator, right)], order='id')
                        operator = 'in'
                    domain = field.determine_domain(model, operator, right)
                    model._flush_search(domain, order='id')

                for elem in normalize_domain(domain):
                    push(elem, model, alias, internal=True)

            # -------------------------------------------------
            # RELATIONAL FIELDS
            # -------------------------------------------------

            # Applying recursivity on field(one2many)
            elif field.type == 'one2many' and operator in HIERARCHY_FUNCS:
                ids2 = to_ids(right, comodel, leaf)
                if field.comodel_name != model._name:
                    dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name)
                else:
                    dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left)
                for dom_leaf in dom:
                    push(dom_leaf, model, alias)

            elif field.type == 'one2many':
                domain = field.get_domain_list(model)
                inverse_is_int = comodel._fields[field.inverse_name].type in ('integer', 'many2one_reference')
                unwrap_inverse = (lambda ids: ids) if inverse_is_int else (lambda recs: recs.ids)

                if right is not False:
                    # determine ids2 in comodel
                    if isinstance(right, str):
                        op2 = (TERM_OPERATORS_NEGATION[operator]
                               if operator in NEGATIVE_TERM_OPERATORS else operator)
                        ids2 = comodel._name_search(right, domain or [], op2, limit=None)
                    elif isinstance(right, collections.abc.Iterable):
                        ids2 = right
                    else:
                        ids2 = [right]
                    if inverse_is_int and domain:
                        ids2 = comodel._search([('id', 'in', ids2)] + domain, order='id')

                    if isinstance(ids2, Query) and comodel._fields[field.inverse_name].store:
                        op1 = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
                        subquery, subparams = ids2.subselect('"%s"."%s"' % (comodel._table, field.inverse_name))
                        push(('id', op1, (subquery, subparams)), model, alias, internal=True)
                    elif ids2 and comodel._fields[field.inverse_name].store:
                        op1 = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
                        subquery = 'SELECT "%s" FROM "%s" WHERE "id" IN %%s' % (field.inverse_name, comodel._table)
                        subparams = [tuple(ids2)]
                        push(('id', op1, (subquery, subparams)), model, alias, internal=True)
                    else:
                        # determine ids1 in model related to ids2
                        recs = comodel.browse(ids2).sudo().with_context(prefetch_fields=False)
                        ids1 = unwrap_inverse(recs.mapped(field.inverse_name))
                        # rewrite condition in terms of ids1
                        op1 = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
                        push(('id', op1, ids1), model, alias)

                else:
                    if comodel._fields[field.inverse_name].store and not (inverse_is_int and domain):
                        # rewrite condition to match records with/without lines
                        op1 = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect'
                        subquery = 'SELECT "%s" FROM "%s" where "%s" is not null' % (field.inverse_name, comodel._table, field.inverse_name)
                        push(('id', op1, (subquery, [])), model, alias, internal=True)
                    else:
                        comodel_domain = [(field.inverse_name, '!=', False)]
                        if inverse_is_int and domain:
                            comodel_domain += domain
                        recs = comodel.search(comodel_domain, order='id').sudo().with_context(prefetch_fields=False)
                        # determine ids1 = records with lines
                        ids1 = unwrap_inverse(recs.mapped(field.inverse_name))
                        # rewrite condition to match records with/without lines
                        op1 = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
                        push(('id', op1, ids1), model, alias)

            elif field.type == 'many2many':
                rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2

                if operator in HIERARCHY_FUNCS:
                    # determine ids2 in comodel
                    ids2 = to_ids(right, comodel, leaf)
                    domain = HIERARCHY_FUNCS[operator]('id', ids2, comodel)
                    ids2 = comodel._search(domain, order='id')

                    # rewrite condition in terms of ids2
                    if comodel == model:
                        push(('id', 'in', ids2), model, alias)
                    else:
                        subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
                        push(('id', 'inselect', (subquery, [tuple(ids2) or (None,)])), model, alias, internal=True)

                elif right is not False:
                    # determine ids2 in comodel
                    if isinstance(right, str):
                        domain = field.get_domain_list(model)
                        op2 = (TERM_OPERATORS_NEGATION[operator]
                               if operator in NEGATIVE_TERM_OPERATORS else operator)
                        ids2 = comodel._name_search(right, domain or [], op2, limit=None)
                    elif isinstance(right, collections.abc.Iterable):
                        ids2 = right
                    else:
                        ids2 = [right]

                    if isinstance(ids2, Query):
                        # rewrite condition in terms of ids2
                        subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
                        subquery, subparams = ids2.subselect()
                        query = 'SELECT "%s" FROM "%s" WHERE "%s" IN (%s)' % (rel_id1, rel_table, rel_id2, subquery)
                        push(('id', subop, (query, subparams)), model, alias, internal=True)
                    else:
                        # rewrite condition in terms of ids2
                        subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
                        subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
                        ids2 = tuple(it for it in ids2 if it) or (None,)
                        push(('id', subop, (subquery, [ids2])), model, alias, internal=True)

                else:
                    # rewrite condition to match records with/without relations
                    op1 = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect'
                    subquery = 'SELECT "%s" FROM "%s" where "%s" is not null' % (rel_id1, rel_table, rel_id1)
                    push(('id', op1, (subquery, [])), model, alias, internal=True)

            elif field.type == 'many2one':
                if operator in HIERARCHY_FUNCS:
                    ids2 = to_ids(right, comodel, leaf)
                    if field.comodel_name != model._name:
                        dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name)
                    else:
                        dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left)
                    for dom_leaf in dom:
                        push(dom_leaf, model, alias)
                else:
                    def _get_expression(comodel, left, right, operator):
                        #Special treatment to ill-formed domains
                        operator = (operator in ['<', '>', '<=', '>=']) and 'in' or operator

                        dict_op = {'not in': '!=', 'in': '=', '=': 'in', '!=': 'not in'}
                        if isinstance(right, tuple):
                            right = list(right)
                        if (not isinstance(right, list)) and operator in ['not in', 'in']:
                            operator = dict_op[operator]
                        elif isinstance(right, list) and operator in ['!=', '=']:  # for domain (FIELD,'=',['value1','value2'])
                            operator = dict_op[operator]
                        res_ids = comodel.with_context(active_test=False)._name_search(right, [], operator, limit=None)
                        if operator in NEGATIVE_TERM_OPERATORS:
                            res_ids = list(res_ids) + [False]  # TODO this should not be appended if False was in 'right'
                        return left, 'in', res_ids
                    # resolve string-based m2o criterion into IDs
                    if isinstance(right, str) or \
                            isinstance(right, (tuple, list)) and right and all(isinstance(item, str) for item in right):
                        push(_get_expression(comodel, left, right, operator), model, alias)
                    else:
                        # right == [] or right == False and all other cases are handled by __leaf_to_sql()
                        expr, params = self.__leaf_to_sql(leaf, model, alias)
                        push_result(expr, params)

            # -------------------------------------------------
            # BINARY FIELDS STORED IN ATTACHMENT
            # -> check for null only
            # -------------------------------------------------

            elif field.type == 'binary' and field.attachment:
                if operator in ('=', '!=') and not right:
                    inselect_operator = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect'
                    subselect = "SELECT res_id FROM ir_attachment WHERE res_model=%s AND res_field=%s"
                    params = (model._name, left)
                    push(('id', inselect_operator, (subselect, params)), model, alias, internal=True)
                else:
                    _logger.error("Binary field '%s' stored in attachment: ignore %s %s %s",
                                  field.string, left, operator, reprlib.repr(right))
                    push(TRUE_LEAF, model, alias)

            # -------------------------------------------------
            # OTHER FIELDS
            # -> datetime fields: manage time part of the datetime
            #    column when it is not there
            # -> manage translatable fields
            # -------------------------------------------------

            else:
                if field.type == 'datetime' and right:
                    if isinstance(right, str) and len(right) == 10:
                        if operator in ('>', '<='):
                            right += ' 23:59:59'
                        else:
                            right += ' 00:00:00'
                        push((left, operator, right), model, alias)
                    elif isinstance(right, date) and not isinstance(right, datetime):
                        if operator in ('>', '<='):
                            right = datetime.combine(right, time.max)
                        else:
                            right = datetime.combine(right, time.min)
                        push((left, operator, right), model, alias)
                    else:
                        expr, params = self.__leaf_to_sql(leaf, model, alias)
                        push_result(expr, params)

                elif field.translate is True and right:
                    need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
                    sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
                    if need_wildcard:
                        right = '%%%s%%' % right
                    if sql_operator in ('in', 'not in'):
                        right = tuple(right)

                    unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x

                    left = unaccent(model._generate_translated_field(alias, left, self.query))
                    instr = unaccent('%s')
                    push_result(f"{left} {sql_operator} {instr}", [right])

                else:
                    expr, params = self.__leaf_to_sql(leaf, model, alias)
                    push_result(expr, params)

        # ----------------------------------------
        # END OF PARSING FULL DOMAIN
        # -> put result in self.result and self.query
        # ----------------------------------------

        [self.result] = result_stack
        where_clause, where_params = self.result
        self.query.add_where(where_clause, where_params)

    def __leaf_to_sql(self, leaf, model, alias):
        left, operator, right = leaf

        # final sanity checks - should never fail
        assert operator in (TERM_OPERATORS + ('inselect', 'not inselect')), \
            "Invalid operator %r in domain term %r" % (operator, leaf)
        assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields, \
            "Invalid field %r in domain term %r" % (left, leaf)
        assert not isinstance(right, BaseModel), \
            "Invalid value %r in domain term %r" % (right, leaf)

        table_alias = '"%s"' % alias

        if leaf == TRUE_LEAF:
            query = 'TRUE'
            params = []

        elif leaf == FALSE_LEAF:
            query = 'FALSE'
            params = []

        elif operator == 'inselect':
            query = '(%s."%s" in (%s))' % (table_alias, left, right[0])
            params = list(right[1])

        elif operator == 'not inselect':
            query = '(%s."%s" not in (%s))' % (table_alias, left, right[0])
            params = list(right[1])

        elif operator in ['in', 'not in']:
            # Two cases: right is a boolean or a list. The boolean case is an
            # abuse and handled for backward compatibility.
            if isinstance(right, bool):
                _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,))
                if (operator == 'in' and right) or (operator == 'not in' and not right):
                    query = '(%s."%s" IS NOT NULL)' % (table_alias, left)
                else:
                    query = '(%s."%s" IS NULL)' % (table_alias, left)
                params = []
            elif isinstance(right, Query):
                subquery, subparams = right.subselect()
                query = '(%s."%s" %s (%s))' % (table_alias, left, operator, subquery)
                params = subparams
            elif isinstance(right, (list, tuple)):
                if model._fields[left].type == "boolean":
                    params = [it for it in (True, False) if it in right]
                    check_null = False in right
                else:
                    params = [it for it in right if it != False]
                    check_null = len(params) < len(right)
                if params:
                    if left == 'id':
                        instr = ','.join(['%s'] * len(params))
                    else:
                        field = model._fields[left]
                        instr = ','.join([field.column_format] * len(params))
                        params = [field.convert_to_column(p, model, validate=False) for p in params]
                    query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr)
                else:
                    # The case for (left, 'in', []) or (left, 'not in', []).
                    query = 'FALSE' if operator == 'in' else 'TRUE'
                if (operator == 'in' and check_null) or (operator == 'not in' and not check_null):
                    query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)
                elif operator == 'not in' and check_null:
                    query = '(%s AND %s."%s" IS NOT NULL)' % (query, table_alias, left)  # needed only for TRUE.
            else:  # Must not happen
                raise ValueError("Invalid domain term %r" % (leaf,))

        elif left in model and model._fields[left].type == "boolean" and ((operator == '=' and right is False) or (operator == '!=' and right is True)):
            query = '(%s."%s" IS NULL or %s."%s" = false )' % (table_alias, left, table_alias, left)
            params = []

        elif (right is False or right is None) and (operator == '='):
            query = '%s."%s" IS NULL ' % (table_alias, left)
            params = []

        elif left in model and model._fields[left].type == "boolean" and ((operator == '!=' and right is False) or (operator == '==' and right is True)):
            query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table_alias, left, table_alias, left)
            params = []

        elif (right is False or right is None) and (operator == '!='):
            query = '%s."%s" IS NOT NULL' % (table_alias, left)
            params = []

        elif operator == '=?':
            if right is False or right is None:
                # '=?' is a short-circuit that makes the term TRUE if right is None or False
                query = 'TRUE'
                params = []
            else:
                # '=?' behaves like '=' in other cases
                query, params = self.__leaf_to_sql((left, '=', right), model, alias)

        else:
            need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
            sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
            cast = '::text' if  sql_operator.endswith('like') else ''

            if left not in model:
                raise ValueError("Invalid field %r in domain term %r" % (left, leaf))
            format = '%s' if need_wildcard else model._fields[left].column_format
            unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x
            column = '%s.%s' % (table_alias, _quote(left))
            query = '(%s %s %s)' % (unaccent(column + cast), sql_operator, unaccent(format))

            if (need_wildcard and not right) or (right and operator in NEGATIVE_TERM_OPERATORS):
                query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)

            if need_wildcard:
                params = ['%%%s%%' % pycompat.to_text(right)]
            else:
                field = model._fields[left]
                params = [field.convert_to_column(right, model, validate=False)]

        return query, params

    def to_sql(self):
        warnings.warn("deprecated expression.to_sql(), use expression.query instead",
                      DeprecationWarning)
        return self.result