Tutorial

This tutorial demonstrates some features of Odooly in the interactive shell.

It assumes an Odoo server is installed. The shell is a true Python shell. We have access to all the features and modules of the Python interpreter.

First connection

The server is freshly installed and does not have an Odoo database yet. The tutorial creates its own database demo to play with.

Open the Odooly shell:

~$ odooly

It assumes that the server is running locally, and listens on default port 8069.

If our configuration is different, then we use arguments, like:

~$ odooly http://192.168.0.42:8069

It connects using the Webclient API.

In case we use a different protocol, we can set the endpoint explicitly. For example /jsonrpc for the JSON-RPC API:

~$ odooly http://127.0.0.1:8069/jsonrpc

Note

Protocols JSON-RPC and XML-RPC are deprecated in Odoo 19 and will be removed in Odoo 22. Odooly 2.5.8 has support for XML-RPC and older versions of Odoo.

On login, it prints few lines about the commands available.

~$ odooly
Usage (some commands):
    env[name]                       # Return a Model instance
    env[name].keys()                # List field names of the model
    env[name].fields(names=None)    # Return details for the fields
    env[name].field(name)           # Return details for the field
    env[name].browse(ids=())
    env[name].search(domain)
    env[name].search(domain, offset=0, limit=None, order=None)
                                    # Return a RecordList

    rec = env[name].get(domain)     # Get the Record matching domain
    rec.some_field                  # Return the value of this field
    rec.read(fields=None)           # Return values for the fields

    client.login(user)              # Login with another user
    client.connect(env_name)        # Connect to another env.
    client.connect(server=None)     # Connect to another server
    env.models(name)                # List models matching pattern
    env.modules(name)               # List modules matching pattern
    env.install(module1, module2, ...)
    env.upgrade(module1, module2, ...)
                                    # Install or upgrade the modules
    env.upgrade_cancel()            # Reset failed upgrade/install

And it confirms that the default database is not available:

...
Error: Database 'odoo' does not exist: []

Though, we have a connected client, ready to use:

>>> client
<Client 'http://localhost:8069/web?db='>
>>> client.server_version
'18.0'
>>> #

Create a database

We create the database "demo" for this tutorial. We need to know the superadmin password before to continue. This is the admin_passwd in the odoo-server.conf file. Default password is "admin".

Note

This password gives full control on the databases. Set a strong password in the configuration to prevent unauthorized access.

>>> client.create_database('super_password', 'demo')
Logged in as 'admin'
>>> client
<Client 'http://localhost:8069/web?db=demo'>
>>> client.database.list()
['demo']
>>> env
<Env 'admin@demo'>
>>> env.modules(installed=True)
{'installed': ['base', 'web', 'web_mobile', 'web_tests']}
>>> len(env.modules()['uninstalled'])
1398
>>> #

Note

Create an odooly.ini file in the current directory to declare all our environments. Example:

[demo]
server = http://localhost:8069/web
database = demo
username = joe

Then we connect to any environment with odooly --env demo or switch during an interactive session with client.connect('demo').

Clone a database

It is sometimes useful to clone a database (testing, backup, migration, …). A shortcut is available for that, the required parameters are the new database name and the superadmin password.

>>> client.clone_database('super_password', 'demo_test')
Logged in as 'admin'
>>> client
<Client 'http://localhost:8069/web?db=demo_test'>
>>> client.database.list()
['demo', 'demo_test']
>>> env
<Env 'admin@demo_test'>
>>> client.modules(installed=True)
{'installed': ['base', 'web', 'web_mobile', 'web_tests']}
>>> len(client.modules()['uninstalled'])
1398
>>> #

Find the users

We have created the database "demo" for the tests. We are connected to this database as 'admin'.

Where is the table for the users?

>>> client
<Client 'http://localhost:8069/web?db=demo'>
>>> env.models('user')
['res.users', 'res.users.log']

We’ve listed two models which matches the name, res.users and res.users.log. Through the environment Env we reach the users’ model and we want to introspect its fields. Fortunately, the Model class provides methods to retrieve all the details.

>>> env['res.users']
<Model 'res.users'>
>>> print(env['res.users'].keys())
['action_id', 'active', 'company_id', 'company_ids', 'context_lang',
 'context_tz', 'date', 'group_ids', 'id', 'login', 'menu_id', 'menu_tips',
 'name', 'new_password', 'password', 'signature', 'user_email', 'view']
>>> env['res.users'].field('company_id')
{'change_default': False,
 'company_dependent': False,
 'context': {'user_preference': True},
 'depends': [],
 'domain': [],
 'help': 'The company this user is currently working for.',
 'manual': False,
 'readonly': False,
 'relation': 'res.company',
 'required': True,
 'searchable': True,
 'sortable': True,
 'store': True,
 'string': 'Company',
 'type': 'many2one'}
>>> #

Let’s examine the 'admin' user in details.

>>> env['res.users'].search_count()
1
>>> admin_user = env['res.users'].browse(1)
>>> admin_user.groups_id
<RecordList 'res.groups,length=7'>
>>> admin_user.groups_id.full_name
['Administration / Access Rights',
 'Technical / Access to export feature',
 'Bypass HTML Field Sanitize',
 'Extra Rights / Contact Creation',
 'User types / Internal User',
 'Administration / Settings',
 'Extra Rights / Technical Features']
>>> admin_user.get_metadata()
[{'create_date': '2024-10-01 10:08:20',
  'create_uid': False,
  'id': 1,
  'noupdate': True,
  'write_date': '2024-10-01 10:08:30',
  'write_uid': [1, 'System'],
  'xmlid': 'base.user_root',
  'xmlids': [{'noupdate': True, 'xmlid': 'base.user_root'}]}]

Create a new user

Now we want a non-admin user to continue the exploration. Let’s create Joe.

>>> env['res.users'].create({'name': 'Joe'})
odoo.exceptions.ValidationError: The operation cannot be completed:
- Create/update: a mandatory field is not set.
- Delete: another model requires the record being deleted. If possible, archive it instead.

Model: User (res.users)
Field: Login (login)
>>> #

It seems we’ve forgotten some mandatory data. Let’s give him a name and a login.

>>> env['res.users'].create({'login': 'joe', 'name': 'Joe'})
<Record 'res.users,3'>
>>> joe_user = _
>>> joe_user.groups_id.full_name
['Technical / Access to export feature',
 'Extra Rights / Contact Creation',
 'User types / Internal User',
 'Extra Rights / Technical Features']

The user Joe does not have a password: we cannot login as joe. We set a password for Joe and we try again.

>>> client.login('joe')
Password for 'joe':
Error: Invalid username or password
>>> env.user.login
'admin'
>>> joe_user.password = 'bartender'
>>> client.login('joe')
Logged in as 'joe'
>>> env.user.login
'joe'
>>> #

Success!

Explore the model

We keep connected as user Joe and we explore the world around us.

>>> env.user.login
'joe'
>>> all_models = env.models()
odoo.exceptions.AccessError: You are not allowed to access 'Models' (ir.model) records.

This operation is allowed for the following groups:
        - Administration/Access Rights

Contact your administrator to request access if necessary.
>>> all_models = env.sudo().models()
>>> len(all_models)
140

Among these 140 objects, some of them are read-only, others are read-write. We can also filter the non-empty models.

>>> # Read-only models
>>> len([m for m in all_models if not env[m].access('write')])
116
>>> #
>>> # Writable but cannot delete
>>> [m for m in all_models if env[m].access('write') and not env[m].access('unlink')]
['base.language.export',
 'base.partner.merge.automatic.wizard',
 'base_import.import',
 'res.users.identitycheck']
>>> #
>>> # Unreadable models
>>> len([m for m in all_models if not env[m].access('read')])
94
>>> #
>>> # Now print the number of entries in all (readable) models
>>> for m in all_models:
...     if m == 'res.users.apikeys.show':
...         continue  # This one returns an error
...     mcount = env[m].access() and env[m].search_count()
...     if not mcount:
...         continue
...     print('%4d  %s' % (mcount, m))
...
   1  iap.service
   1  ir.attachment
   1  ir.default
  22  ir.ui.menu
   7  report.layout
   3  report.paperformat
   2  res.bank
   1  res.company
 250  res.country
   6  res.country.group
1780  res.country.state
   1  res.currency
 162  res.currency.rate
  11  res.groups
   1  res.lang
  38  res.partner
   1  res.partner.bank
   7  res.partner.category
  21  res.partner.industry
   5  res.partner.title
   4  res.users
   1  res.users.settings
>>> #
>>> # Show the content of a model
>>> config_params = env['ir.config_parameter'].sudo().search([])
>>> config_params.read('[{id:2}]  {key:30} {value}')
['[ 8]  base.default_max_email_size    10',
 '[ 5]  base.login_cooldown_after      10',
 '[ 6]  base.login_cooldown_duration   60',
 '[ 7]  base.template_portal_user_id   5',
 '[10]  base_setup.default_user_rights True',
 '[ 9]  base_setup.show_effect         True',
 '[ 3]  database.create_date           2024-10-01 06:10:24',
 '[ 1]  database.secret                88888888-8888-8888-8888-888888888888',
 '[ 2]  database.uuid                  77777777-7777-7777-7777-777777777777',
 '[ 4]  web.base.url                   http://localhost:8069']

Browse the records

Query the "res.country" model:

>>> env['res.country'].keys()
['address_format', 'code', 'name']
>>> env['res.country'].search(['name like public'])
<RecordList 'res.country,[...]'>
>>> env['res.country'].search(['name like public']).name
['Central African Republic',
 'Czech Republic',
 'Democratic Republic of the Congo',
 'Dominican Republic']
>>> env['res.country'].search(['code > X'], order='code ASC').read('{code} {name}')
['XK Kosovo',
 'YE Yemen',
 'YT Mayotte',
 'ZA South Africa',
 'ZM Zambia',
 'ZW Zimbabwe']
>>> #

… the tutorial is done.

Jump to the Odooly API for further details.