How to set language for controller


Imagine that you have a signup procedure, like this:

class MobileAppBaseController(http.Controller):
  @http.route('/barrier/verify_user/1', type='json', auth='public',
              methods=['POST'], csrf=False)
  def verify_user_step_1(self):
      user_name = http.request.jsonrequest.get('username')
      if not user_name:
          return {'status': False, 'msg': _('User name not sent.')}
      ...

When user name is not specified in JSON request it returns 'User name not sent' message.

Now we want this message to be returned in different languages.

After learning fron odoo/tools/translation.py and odoo/http.py two solutions where found.

1. Language selection by client

To request messages translated into specified language Accept-Language header is used:

http -vv --json 'http://127.0.0.1:38069/barrier/verify_user/1' "_username=user" 'Accept-Language:ru'

Result:

odooist@MacBook-Pro app % http -v --json 'http://127.0.0.1:38069/barrier/verify_user/1' "_username=user" 'Accept-Language:ru'
POST /barrier/verify_user/1 HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Accept-Language: ru
Connection: keep-alive
Content-Length: 21
Content-Type: application/json
Host: 127.0.0.1:38069
User-Agent: HTTPie/1.0.3

 {
    "_username": "user"
 }

HTTP/1.0 200 OK
Content-Length: 224
Content-Type: application/json
Date: Sat, 28 Dec 2019 11:33:49 GMT
Server: Werkzeug/0.16.0 Python/3.5.3
Set-Cookie: session_id=7b62bc401c248052e8a0ffb7626b7122340f774a; Expires=Fri, 27-Mar-2020 11:33:49 GMT; Max-Age=7776000; HttpOnly; Path=/

{
  "id": null,
  "jsonrpc": "2.0",
  "result": {
      "msg": "Имя пользователя не отослано.",
      "status": false
  }
}

As you can see in request headers Accept-Language is set to ru, and in result we got msg translated.

2. Define response language at server

In odoo/tools/translation.py interesting code was found:

def _get_lang(self, frame):
    # try, in order: context.get('lang'), kwargs['context'].get('lang'),
    # self.env.lang, self.localcontext.get('lang'), request.env.lang
    lang = None
    if frame.f_locals.get('context'):
        lang = frame.f_locals['context'].get('lang')
    if not lang:
        kwargs = frame.f_locals.get('kwargs', {})
        if kwargs.get('context'):
            lang = kwargs['context'].get('lang')
    if not lang:
        s = frame.f_locals.get('self')
        if hasattr(s, 'env'):
            lang = s.env.lang
        if not lang:
            if hasattr(s, 'localcontext'):
                lang = s.localcontext.get('lang')
        if not lang:
            try:
                from odoo.http import request
                lang = request.env.lang
            except RuntimeError:
                pass
        if not lang:
            # Last resort: attempt to guess the language of the user
            # Pitfall: some operations are performed in sudo mode, and we
            #          don't know the original uid, so the language may
            #          be wrong when the admin language differs.
            (cr, dummy) = self._get_cr(frame, allow_create=False)
            uid = self._get_uid(frame)
            if cr and uid:
                env = odoo.api.Environment(cr, uid, {})
                lang = env['res.users'].context_get()['lang']
    return lang

So to define server response language we had to put a context dictionary on a function level like this (line 5):

1
2
3
4
5
6
7
8
   class MobileAppBaseController(http.Controller):
     @http.route('/barrier/verify_user/1', type='json', auth='public',
                 methods=['POST'], csrf=False)
     def verify_user_step_1(self):
         context = {'language': 'ru_RU'}
         user_name = http.request.jsonrequest.get('username')
         if not user_name:
             return {'status': False, 'msg': _('User name not sent.')}

Notice that in the first case we added Accept-Language: ru, but in the second one we set language to ru_RU.

Working with Savepoint


Here I'll write about Odoo transactions.

It's a general recommendation to let Odoo manage transactions and Odoo does it quite well.

By default a new transaction is opened on every HTTP request thus covering the whole bunch of operations the method executed.

If an exception happends during any method execution the whole transation is rolled back and error is returned.

But sometimes we need to keep some amount of work done in the middle of request and if the rest of the request fails we are saved.

This is done by using self.env.cr.commit() call.

Savepoint

Savepoint defines a new saving point in current transation. So when an error occurs the transation is rolled back to that point.

If no error happens RELEASE SAVEPOINT is called to keep the effects of commands executed after the savepoint was established.

Here is the code snippet from odoo/sql_db.py:

@contextmanager
@check
def savepoint(self, flush=True):
    """context manager entering in a new savepoint"""
    name = uuid.uuid1().hex
    if flush:
        flush_env(self)
    self.execute('SAVEPOINT "%s"' % name)
    try:
        yield
        if flush:
            flush_env(self)
    except Exception:
        if flush:
            clear_env(self)
        self.execute('ROLLBACK TO SAVEPOINT "%s"' % name)
        raise
    else:
        self.execute('RELEASE SAVEPOINT "%s"' % name)

Form's initial mode


When you click on a tree view row a form view is opened and this form is opened in read mode.

When you click the Create button from a tree view, a new record form view is opened in edit mode (how otherwise? :-)

But sometimes it's more usable to immediately open an existing record in edit mode.

This can be done using form option initial_mode.

In different Odoo versions this can be done in different ways:

10.0

In this version it can work in form declaration (by chance as developers say) like in the following example:

<form options="{'initial_mode': 'edit'}">

If transition from tree to form is done within python code:

return {
    'name': action.name,
    'help': action.help,
    'type': action.type,
    'views': [(form_id, 'form')],
    'view_mode': 'action.view_mode',
    'target': action.target,
    'context': action.context,
    'res_model': action.res_model,
    'flags': {'initial_mode': 'edit'},
    'res_id': record.id
}

11.0, 12.0, ...

Next Odoo versions "fix" the above and now you should use context:

context = dict(self.env.context)
context['form_view_initial_mode'] = 'edit'
return {
    'type': 'ir.actions.act_window',
    'view_type': 'form',
    'view_mode': 'form',
    'res_model': 'your.model',
    'res_id': your_object_id,
    'context': context,
}

🙈

New xpath element named "move"


The position='move' has been introduced to move an element in an inherited view.

It's used as

<xpath expr="//@target" position="after">
    <xpath expr="//@node" position="move"/>
</xpath>

Or also:

<field name="target_field" position="after">
    <field name="my_field" position="move"/>
</field>

In the above example my_field is placed after target_field in the parent form.

So now we can inherit and have the ability to manipulate fields order in views, not only add new fields.

Hiding entire tree column with column_invisible


We all know a way to hide a field in tree & form views using attrs={'invisible': [(...)]}.

We also know that we can hide entire column using invisible="1" attribute like this:

<tree decoration-warning="event_input_mode == True">
    <field name="event_input_mode" invisible="1"/>
    <field name="description"/>
    <field name="facility" invisible="context.get('hide_facility')"/>
    <field name="uid"/>
</tree>

In the above example we need event_input_mode column just to highlight a row if it have event_input_mode set so we hide it with invisible="1".

We can also notice that we can use context values in condition expression. In the above example if context has hide_facility key then entire column is hidden.

The treasure

But what if you need to hide entire column by a condition coming from complex business logic implemented in Python?

Here column_invisible attribute comes!

See this examples:

<tree>
    ...
    <field name="foo" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
    ...
</tree>

By digging the source code I can see column_invisible was introduced in Odoo 11.0.

Nice, but works only for many2one and one2many fields :-)