diff --git a/content/administration/odoo_online.rst b/content/administration/odoo_online.rst index 5d16c073ce..f771328f93 100644 --- a/content/administration/odoo_online.rst +++ b/content/administration/odoo_online.rst @@ -160,49 +160,20 @@ Web Services ============ In order to programmatically retrieve the list of the databases displayed in the -`database manager `_, call the method `list` of the model -`odoo.database` via a :doc:`Web Service ` call. +`database manager `_, call the method ``list`` of the model +``odoo.database`` via a :doc:`/developer/reference/external_api` call. -Inspired from the examples provided in the :doc:`Web Services ` -section, this is how to retrieve this list with the library ``xmlrpc.client``:: +Example:: - import xmlrpc.client + import requests - USER = 'user@domain.tld' - APIKEY = 'your_apikey' + APIKEY = "your_apikey" - root = 'https://www.odoo.com/xmlrpc/' - uid = xmlrpc.client.ServerProxy(root + 'common').login('openerp', USER, APIKEY) - sock = xmlrpc.client.ServerProxy(root + 'object') - databases_list = sock.execute('openerp', uid, APIKEY, 'odoo.database', 'list') - -And here is the equivalent example with JSON-RPC:: - - import json - import random - import urllib.request - - USER = 'user@domain.tld' - APIKEY = 'your_apikey' - - def json_rpc(url, method, params): - data = { - 'jsonrpc': '2.0', - 'method': method, - 'params': params, - 'id': random.randint(0, 1000000000), + requests.post( + "https://www.odoo.com/json/2/odoo.database/list", + headers={ + "Authorization": f"bearer {APIKEY}", + "X-Odoo-Database": "openerp", } - req = urllib.request.Request(url=url, data=json.dumps(data).encode(), headers={ - "Content-Type": "application/json", - }) - reply = json.loads(urllib.request.urlopen(req).read().decode('UTF-8')) - if reply.get('error'): - raise Exception(reply['error']) - return reply['result'] - - def call(url, service, method, *args): - return json_rpc(url, 'call', {'service': service, 'method': method, 'args': args}) - - url = 'https://www.odoo.com/jsonrpc' - uid = call(url, 'common', 'login', 'openerp', USER, APIKEY) - databases_list = call(url, 'object', 'execute', 'openerp', uid, APIKEY, 'odoo.database', 'list') + json={}, + ) diff --git a/content/developer/howtos.rst b/content/developer/howtos.rst index f9a14f24e3..eed9af1718 100644 --- a/content/developer/howtos.rst +++ b/content/developer/howtos.rst @@ -14,7 +14,6 @@ How-to guides howtos/frontend_owl_components howtos/website_themes - howtos/web_services howtos/company howtos/create_reports howtos/accounting_localization @@ -69,11 +68,6 @@ Server-side development .. cards:: - .. card:: Web services - :target: howtos/web_services - - Learn more about Odoo's web services. - .. card:: Multi-company guidelines :target: howtos/company diff --git a/content/developer/howtos/web_services.rst b/content/developer/howtos/web_services.rst deleted file mode 100644 index 14f8deae33..0000000000 --- a/content/developer/howtos/web_services.rst +++ /dev/null @@ -1,148 +0,0 @@ -============ -Web Services -============ - -The web-service module offers a common interface for all web services: - -- XML-RPC -- JSON-RPC - -Business objects can also be accessed via the distributed object -mechanism. They can all be modified via the client interface with contextual -views. - -Odoo is accessible through XML-RPC/JSON-RPC interfaces, for which libraries -exist in many languages. - -XML-RPC Library ---------------- - -The following example is a Python 3 program that interacts with an Odoo -server with the library ``xmlrpc.client``:: - - import xmlrpc.client - - root = 'http://%s:%d/xmlrpc/' % (HOST, PORT) - - uid = xmlrpc.client.ServerProxy(root + 'common').login(DB, USER, PASS) - print("Logged in as %s (uid: %d)" % (USER, uid)) - - # Create a new note - sock = xmlrpc.client.ServerProxy(root + 'object') - args = { - 'color' : 8, - 'memo' : 'This is a note', - 'create_uid': uid, - } - note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args) - -.. exercise:: Add a new service to the client - - Write a Python program able to send XML-RPC requests to a PC running - Odoo (yours, or your instructor's). This program should display all - the sessions, and their corresponding number of seats. It should also - create a new session for one of the courses. - - .. only:: solutions - - .. code-block:: python - - import functools - import xmlrpc.client - HOST = 'localhost' - PORT = 8069 - DB = 'openacademy' - USER = 'admin' - PASS = 'admin' - ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT) - - # 1. Login - uid = xmlrpc.client.ServerProxy(ROOT + 'common').login(DB,USER,PASS) - print("Logged in as %s (uid:%d)" % (USER,uid)) - - call = functools.partial( - xmlrpc.client.ServerProxy(ROOT + 'object').execute, - DB, uid, PASS) - - # 2. Read the sessions - sessions = call('openacademy.session','search_read', [], ['name','seats']) - for session in sessions: - print("Session %s (%s seats)" % (session['name'], session['seats'])) - # 3.create a new session - session_id = call('openacademy.session', 'create', { - 'name' : 'My session', - 'course_id' : 2, - }) - - Instead of using a hard-coded course id, the code can look up a course - by name:: - - # 3.create a new session for the "Functional" course - course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0] - session_id = call('openacademy.session', 'create', { - 'name' : 'My session', - 'course_id' : course_id, - }) - -.. seealso:: - - :doc:`../reference/external_api`: The in-depth tutorial on XML-RPC, with examples spanning multiple programming languages. - -JSON-RPC Library ----------------- - -The following example is a Python 3 program that interacts with an Odoo server -with the standard Python libraries ``urllib.request`` and ``json``. This -example assumes the **Productivity** app (``note``) is installed:: - - import json - import random - import urllib.request - - HOST = 'localhost' - PORT = 8069 - DB = 'openacademy' - USER = 'admin' - PASS = 'admin' - - def json_rpc(url, method, params): - data = { - "jsonrpc": "2.0", - "method": method, - "params": params, - "id": random.randint(0, 1000000000), - } - req = urllib.request.Request(url=url, data=json.dumps(data).encode(), headers={ - "Content-Type":"application/json", - }) - reply = json.loads(urllib.request.urlopen(req).read().decode('UTF-8')) - if reply.get("error"): - raise Exception(reply["error"]) - return reply["result"] - - def call(url, service, method, *args): - return json_rpc(url, "call", {"service": service, "method": method, "args": args}) - - # log in the given database - url = "http://%s:%s/jsonrpc" % (HOST, PORT) - uid = call(url, "common", "login", DB, USER, PASS) - - # create a new note - args = { - 'color': 8, - 'memo': 'This is another note', - 'create_uid': uid, - } - note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args) - -Examples can be easily adapted from XML-RPC to JSON-RPC. - -.. note:: - - There are a number of high-level APIs in various languages to access Odoo - systems without *explicitly* going through XML-RPC or JSON-RPC, such as: - - * https://github.com/akretion/ooor - * https://github.com/OCA/odoorpc - * https://github.com/nicolas-van/openerp-client-lib - * http://pythonhosted.org/OdooRPC - * https://github.com/abhishek-jaiswal/php-openerp-lib diff --git a/content/developer/reference.rst b/content/developer/reference.rst index af531f28ca..65345865b9 100644 --- a/content/developer/reference.rst +++ b/content/developer/reference.rst @@ -17,4 +17,5 @@ Reference reference/cli reference/upgrades reference/external_api + reference/external_rpc_api reference/extract_api diff --git a/content/developer/reference/external_api.rst b/content/developer/reference/external_api.rst index ba37db0cec..6ddac35de3 100644 --- a/content/developer/reference/external_api.rst +++ b/content/developer/reference/external_api.rst @@ -1,1462 +1,541 @@ -============ -External API -============ +=================== +External JSON-2 API +=================== + +.. versionadded:: 19.0 -Odoo is usually extended internally via modules, but many of its features and -all of its data are also available from the outside for external analysis or -integration with various tools. Part of the :ref:`reference/orm/model` API is -easily available over XML-RPC_ and accessible from a variety of languages. +Odoo is usually extended internally via modules, but many of its features and all of its data are +also available externally for analysis or integration with various other softwares. Part of the +:ref:`reference/orm/model` API is easily available over HTTP via the ``/json/2`` endpoint. -.. important:: - Starting with PHP8, the XML-RPC extension may not be available by default. - Check out the `manual `_ - for the installation steps. +.. note:: + + The actual models, fields and methods available are specific to every database and can be + consulted on their ``/doc`` page. .. note:: + Access to data via the external API is only available on *Custom* Odoo pricing plans. Access to the external API is not available on *One App Free* or *Standard* plans. For more information visit the `Odoo pricing page `_ or reach out to your Customer Success Manager. -.. seealso:: - - :doc:`Tutorial on web services <../howtos/web_services>` - -Connection -========== - -Configuration -------------- - -If you already have an Odoo server installed, you can just use its parameters. - -.. important:: - - For Odoo Online instances (.odoo.com), users are created without a - *local* password (as a person you are logged in via the Odoo Online - authentication system, not by the instance itself). To use XML-RPC on Odoo - Online instances, you will need to set a password on the user account you - want to use: - - * Log in your instance with an administrator account. - * Go to :menuselection:`Settings --> Users & Companies --> Users`. - * Click on the user you want to use for XML-RPC access. - * Click on :guilabel:`Action` and select :guilabel:`Change Password`. - * Set a :guilabel:`New Password` value then click :guilabel:`Change Password`. - - The *server url* is the instance's domain (e.g. - *https://mycompany.odoo.com*), the *database name* is the name of the - instance (e.g. *mycompany*). The *username* is the configured user's login - as shown by the *Change Password* screen. - -.. tabs:: - - .. code-tab:: python - - url = - db = - username = 'admin' - password = - - .. code-tab:: ruby - - url = - db = - username = "admin" - password = - - .. code-tab:: php +API +=== - $url = ; - $db = ; - $username = "admin"; - $password = ; +Request +------- - .. code-tab:: java +Post a JSON object at the ``/json/2//`` URL. - final String url = , - db = , - username = "admin", - password = ; +**HTTP Headers** - .. code-tab:: go +:Host: Required, the hostname of the server. +:Autorization: Required, ``bearer`` followed by an API key. +:Content-Type: Required, ``application/json``, a charset is recommended. +:X-Odoo-Dataase: Optional, the name of the database on which to connect. +:User-Agent: Recommended, the name of your software. - var ( - url = - db = - username = "admin" - password = - ) +**URL Path** -.. _api/external_api/keys: +:model: Required, the technical model name. +:method: Required, the method to execute. -API Keys -~~~~~~~~ +**Body JSON object** -.. versionadded:: 14.0 +:ids: An array of record ids on which to execute the method. +:context: Optional, an object of additional values. e.g. ``{"lang": "en_US"}``. +:*param*: As many time as needed, a value for the method's *param* parameter. -Odoo has support for **api keys** and (depending on modules or settings) may -**require** these keys to perform webservice operations. +The ``ids`` entry must be empty when calling a ``@api.model``-decorated method. -The way to use API Keys in your scripts is to simply replace your **password** -by the key. The login remains in-use. You should store the API Key as carefully -as the password as they essentially provide the same access to your user -account (although they can not be used to log-in via the interface). +**Example** -In order to add a key to your account, simply go to your -:guilabel:`Preferences` (or :guilabel:`My Profile`): +.. code:: http -.. image:: external_api/preferences.png - :align: center + POST /json/2/res.partner/search_read HTTP/1.1 + Host: mycompany.example.com + X-Odoo-Database: mycompany + Authorization: bearer 6578616d706c65206a736f6e20617069206b6579 + Content-Type: application/json; charset=utf-8 + User-Agent: mysoftware python-requests/2.25.1 -then open the :guilabel:`Account Security` tab, and click -:guilabel:`New API Key`: + { + "context": { + "lang": "en_US" + }, + "domain": [ + ["name", "ilike", "%deco%"], + ["is_company", "=", true] + ], + "fields": ["name"] + } -.. image:: external_api/account-security.png - :align: center +Response +-------- -Input a description for the key, **this description should be as clear and -complete as possible**: it is the only way you will have to identify your keys -later and know whether you should remove them or keep them around. +In case of **success**, a **200** status with the JSON-serialized return value of the called method +in the body. -Click :guilabel:`Generate Key`, then copy the key provided. **Store this key -carefully**: it is equivalent to your password, and just like your password -the system will not be able to retrieve or show the key again later on. If you lose -this key, you will have to create a new one (and probably delete the one you -lost). +.. code:: http -Once you have keys configured on your account, they will appear above the -:guilabel:`New API Key` button, and you will be able to delete them: + HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 -.. image:: external_api/delete-key.png - :align: center + [ + {"id": 25, "name": "Deco Addict"} + ] -**A deleted API key can not be undeleted or re-set**. You will have to generate -a new key and update all the places where you used the old one. - -Test database -~~~~~~~~~~~~~ - -To make exploration simpler, you can also ask https://demo.odoo.com for a test -database: +In case of **error**, a **4xx**/**5xx** status with a JSON-serialized error object in the body. .. tabs:: - .. code-tab:: python + .. code-tab:: http - import xmlrpc.client - info = xmlrpc.client.ServerProxy('https://demo.odoo.com/start').start() - url, db, username, password = info['host'], info['database'], info['user'], info['password'] + HTTP/1.1 401 Unauthorized + Content-Type: application/json; charset=utf-8 - .. code-tab:: ruby + { + "name": "werkzeug.exceptions.Unauthorized", + "message": "Invalid apikey", + "arguments": ["Invalid apikey", 401], + "context": {}, + "debug": "Traceback (most recent call last):\n File \"/opt/Odoo/community/odoo/http.py\", line 2212, in _transactioning\n return service_model.retrying(func, env=self.env)\n File \"/opt/Odoo/community/odoo/service/model.py\", line 176, in retrying\n result = func()\n File \"/opt/Odoo/community/odoo/http.py\", line 2177, in _serve_ir_http\n self.registry['ir.http']._authenticate(rule.endpoint)\n File \"/opt/Odoo/community/odoo/addons/base/models/ir_http.py\", line 274, in _authenticate\n cls._authenticate_explicit(auth)\n File \"/opt/Odoo/community/odoo/addons/base/models/ir_http.py\", line 283, in _authenticate_explicit\n getattr(cls, f'_auth_method_{auth}')()\n File \"/opt/Odoo/community/odoo/addons/base/models/ir_http.py\", line 240, in _auth_method_bearer\n raise werkzeug.exceptions.Unauthorized(\nwerkzeug.exceptions.Unauthorized: 401 Unauthorized: Invalid apikey\n" + } - require "xmlrpc/client" - info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start') - url, db, username, password = info['host'], info['database'], info['user'], info['password'] + .. tab:: Debug + + .. code:: + + Traceback (most recent call last): + File "/opt/Odoo/community/odoo/http.py", line 2212, in _transactioning + return service_model.retrying(func, env=self.env) + File "/opt/Odoo/community/odoo/service/model.py", line 176, in retrying + result = func() + File "/opt/Odoo/community/odoo/http.py", line 2177, in _serve_ir_http + self.registry['ir.http']._authenticate(rule.endpoint) + File "/opt/Odoo/community/odoo/addons/base/models/ir_http.py", line 274, in _authenticate + cls._authenticate_explicit(auth) + File "/opt/Odoo/community/odoo/addons/base/models/ir_http.py", line 283, in _authenticate_explicit + getattr(cls, f'_auth_method_{auth}')() + File "/opt/Odoo/community/odoo/addons/base/models/ir_http.py", line 240, in _auth_method_bearer + raise werkzeug.exceptions.Unauthorized( + werkzeug.exceptions.Unauthorized: 401 Unauthorized: Invalid apikey + +:name: The fully qualified name of the Python exception that occured. +:message: The exception message, usually the same as `arguments[0]`. +:arguments: All the exception arguments. +:context: The context used by the request. +:debug: The exception traceback, for debugging purpose. - .. group-tab:: PHP +Configuration +============= - .. code-block:: php +API Key +------- - require_once('ripcord.php'); - $info = ripcord::client('https://demo.odoo.com/start')->start(); - list($url, $db, $username, $password) = array($info['host'], $info['database'], $info['user'], $info['password']); +An API key must be set in the ``Authorization`` request header, as a bearer token. - .. note:: - These examples use the `Ripcord `_ - library, which provides a simple XML-RPC API. Ripcord requires that - `XML-RPC support be enabled - `_ in your PHP - installation. +Create a new API key for a user via :guilabel:`Preferences`, :guilabel:`Account Security`, and +:guilabel:`New API Key`. - Since calls are performed over - `HTTPS `_, it also requires that - the `OpenSSL extension - `_ be enabled. +.. have the three images appear next to each other +.. list-table:: - .. group-tab:: Java + * - .. image:: external_api/preferences2.png + :align: center - .. code-block:: java + - .. image:: external_api/account-security2.png + :align: center - final XmlRpcClient client = new XmlRpcClient(); + - .. image:: external_api/new-api-key.png + :align: center - final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl(); - start_config.setServerURL(new URL("https://demo.odoo.com/start")); - final Map info = (Map)client.execute( - start_config, "start", emptyList()); +A description and a duration are needed to create a new api key. The description makes it possible +to identify the key, and to determine later whether the key is still in use or should be removed. +The duration determines the lifetime of the key after which the key becomes invalid. It is +recommended to set a short duration (typically 1 day) for interactive usage. It is not possible to +create keys that last for more than 3 months, it means that long lasting keys must be rotated at +least once every 3 months. - final String url = info.get("host"), - db = info.get("database"), - username = info.get("user"), - password = info.get("password"); +The :guilabel:`Generate Key` button creates a strong 160-bits random key. Its value appears on +screen, this is the only time and place the key is visible on screen. It must be copied, kept secret +and stored somewhere secure. If it ever gets compromised or lost, then it must be removed. - .. note:: - These examples use the `Apache XML-RPC library `_. +Please refer to OWASP's `Secrets Management Cheat Sheet`_ for further guidance on the management of +API keys. - The examples do not include imports as these imports couldn't be - pasted in the code. +.. _Secrets Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html#secrets-management-cheat-sheet - .. group-tab:: Go - .. code-block:: go +Access Rights +------------- - client, err := xmlrpc.NewClient("https://demo.odoo.com/start", nil) - if err != nil { - log.Fatal(err) - } - info := map[string]string{} - client.Call("start", nil, &info) - url = info["host"].(string) - db = info["database"].(string) - username = info["user"].(string) - password = info["password"].(string) +The JSON-2 API uses the standard :ref:`security ` model of Odoo. All operations +are validated against the access rights, record rules and field accesses of the user. - .. note:: - These examples use the `github.com/kolo/xmlrpc library `_. +For **interactive usage**, such as discovering the API or running one-time scripts, it is fine to +use a **personal account**. - The examples do not include imports as these imports couldn't be - pasted in the code. +For **extended automated usage**, such as an integration with another software, it is recommended to +create and use **dedicated bot users**. -Logging in ----------- +Using dedicated bot users has several benefits: -Odoo requires users of the API to be authenticated before they can query most -data. +* The minimum required permissions can be granted to the bot, limiting the impact may the API key + gets compromised; +* The password can be set empty to disable login/password authentication, limiting the likelihood + the account gets compromised; +* The :ref:`reference/fields/automatic/log_access` use the bot account. No user is impersonalized. -The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require -authentication, such as the authentication itself or fetching version -information. To verify if the connection information is correct before trying -to authenticate, the simplest call is to ask for the server's version. The -authentication itself is done through the ``authenticate`` function and -returns a user identifier (``uid``) used in authenticated calls instead of -the login. -.. tabs:: +Database +-------- - .. code-tab:: python +Depending on the deployment, the ``Host`` and/or ``X-Odoo-Database`` request headers might be +required. The ``Host`` header is required by HTTP/1.1 and is needed on servers where Odoo is +installed next to other web applications, so a web-server/reverse-proxy is able to route the request +to the Odoo server. The ``X-Odoo-Database`` header is required when a single Odoo server hosts +multiple databases, and that :ref:`dbfilter` wasn't configured to use the ``Host`` header. - common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url)) - common.version() +Most HTTP client libraries automatically set the ``Host`` header using the connection URL. - .. code-tab:: ruby - common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common") - common.call('version') +Transaction +=========== - .. code-tab:: php +All calls to the JSON-2 endpoint run in their own SQL transactions. The transaction are committed in +cases of success, and are rollbacked in case of error. Using the JSON-2 API, it is not possible to +chain multiple calls inside a single transaction. It means that one must be cautious when doing +multiple consecutive calls as the data might be modified/removed by other concurrent transactions +and corrupt the data. This is especially dangerous when doing operations related to reservations, +paiements, and the such. - $common = ripcord::client("$url/xmlrpc/2/common"); - $common->version(); +The solution is to always call a single method that does all the related operations in a same +transaction. This way the data is guaranteed to stays consistent: either everything is done +(success, commit), either nothing is done (error, rollback). - .. code-tab:: java +In the ORM, the ``search_read`` method is one example of a single method that does multiple +operations (``search`` then ``read``) in a single transaction. If another concurrent request is +removing one of the records ``search`` is finding, then there’s a risk the subsequent call to +``read`` fails for a missing record error. Such problem cannot occurs in ``search_read`` as that the +system guarantees a proper isolation in between transactions. - final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl(); - common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url))); - client.execute(common_config, "version", emptyList()); +In business models those methods are often prefixed by ``action_``, such as +``sale.order/action_confirm`` that verifies that a sale order is valid before confirming it. - .. code-tab:: go +.. seealso:: - client, err := xmlrpc.NewClient(fmt.Sprintf("%s/xmlrpc/2/common", url), nil) - if err != nil { - log.Fatal(err) - } - common := map[string]any{} - if err := client.Call("version", nil, &common); err != nil { - log.Fatal(err) - } + PostgreSQL - Transaction Isolation - `Repeatable Read`_ -Result: + .. _repeatable read: https://www.postgresql.org/docs/current/transaction-iso.html#XACT-REPEATABLE-READ -.. code-block:: json - { - "server_version": "13.0", - "server_version_info": [13, 0, 0, "final", 0], - "server_serie": "13.0", - "protocol_version": 1, - } +Code Example +============ +The following examples showcase how to execute two of the :ref:`reference/orm/models/crud` on a fake +database ``mycompany`` hosted on a fake website ``https://mycompany.example.com``. Its comprehensive +documentation would be available at https://mycompany.example.com/doc .. tabs:: .. code-tab:: python - uid = common.authenticate(db, username, password, {}) - - .. code-tab:: ruby - - uid = common.call('authenticate', db, username, password, {}) - - .. code-tab:: php - - $uid = $common->authenticate($db, $username, $password, array()); - - .. code-tab:: java + import requests - int uid = (int)client.execute(common_config, "authenticate", asList(db, username, password, emptyMap())); - - .. code-tab:: go - - var uid int64 - if err := client.Call("authenticate", []any{ - db, username, password, - map[string]any{}, - }, &uid); err != nil { - log.Fatal(err) + BASE_URL = "https://mycompany.example.com/json/2" + API_KEY = ... # get it from a secure location + headers = { + "Authorization": f"bearer {API_KEY}", + "X-Odoo-Database": "mycompany", + "User-Agent": "mysoftware " + requests.utils.default_user_agent(), } -.. _api/external_api/calling_methods: - -Calling methods -=============== - -The second endpoint is ``xmlrpc/2/object``. It is used to call methods of odoo -models via the ``execute_kw`` RPC function. - -Each call to ``execute_kw`` takes the following parameters: - -* the database to use, a string -* the user id (retrieved through ``authenticate``), an integer -* the user's password, a string -* the model name, a string -* the method name, a string -* an array/list of parameters passed by position -* a mapping/dict of parameters to pass by keyword (optional) - -.. example:: - - For instance, to search for records in the ``res.partner`` model, we can call - ``name_search`` with ``name`` passed by position and ``limit`` passed by - keyword (in order to get maximum 10 results): - - .. tabs:: - - .. code-tab:: python - - models = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url)) - models.execute_kw(db, uid, password, 'res.partner', 'name_search', ['foo'], {'limit': 10}) - - .. code-tab:: ruby - - models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy - models.execute_kw(db, uid, password, 'res.partner', 'name_search', ['foo'], {limit: 10}) - - .. code-tab:: php - - $models = ripcord::client("$url/xmlrpc/2/object"); - $models->execute_kw($db, $uid, $password, 'res.partner', 'name_search', array('foo'), array('limit' => 10)); - - .. code-tab:: java - - final XmlRpcClient models = new XmlRpcClient() {{ - setConfig(new XmlRpcClientConfigImpl() {{ - setServerURL(new URL(String.format("%s/xmlrpc/2/object", url))); - }}); - }}; - models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "name_search", - asList("foo"), - new HashMap() {{ put("limit", 10); }} - )); - - .. code-tab:: go - - models, err := xmlrpc.NewClient(fmt.Sprintf("%s/xmlrpc/2/object", url), nil) - if err != nil { - log.Fatal(err) - } - var result bool - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "name_search", - []string{"foo"}, - map[string]bool{"limit": 10}, - }, &result); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - true - -List records ------------- - -Records can be listed and filtered via :meth:`~odoo.models.Model.search`. - -:meth:`~odoo.models.Model.search` takes a mandatory -:ref:`domain ` filter (possibly empty), and returns the -database identifiers of all records matching the filter. - -.. example:: - - To list customer companies, for instance: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]]) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', true]]]) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'res.partner', 'search', array(array(array('is_company', '=', true)))); - - .. code-tab:: java - - asList((Object[])models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "search", - asList(asList( - asList("is_company", "=", true))) - ))); - - .. code-tab:: go - - var records []int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "search", - []any{[]any{ - []any{"is_company", "=", true}, - }}, - }, &records); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - [7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74] - -Pagination -~~~~~~~~~~ - -By default a search will return the ids of all records matching the -condition, which may be a huge number. ``offset`` and ``limit`` parameters are -available to only retrieve a subset of all matched records. - -.. example:: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]], {'offset': 10, 'limit': 5}) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', true]]], {offset: 10, limit: 5}) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'res.partner', 'search', array(array(array('is_company', '=', true))), array('offset'=>10, 'limit'=>5)); - - .. code-tab:: java - - asList((Object[])models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "search", - asList(asList( - asList("is_company", "=", true))), - new HashMap() {{ put("offset", 10); put("limit", 5); }} - ))); - - .. code-tab:: go - - var records []int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "search", - []any{[]any{ - []any{"is_company", "=", true}, - }}, - map[string]int64{"offset": 10, "limit": 5}, - }, &records); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - [13, 20, 30, 22, 29] - -Count records -------------- - -Rather than retrieve a possibly gigantic list of records and count them, -:meth:`~odoo.models.Model.search_count` can be used to retrieve -only the number of records matching the query. It takes the same -:ref:`domain ` filter as -:meth:`~odoo.models.Model.search` and no other parameter. - -.. example:: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'search_count', [[['is_company', '=', True]]]) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'search_count', [[['is_company', '=', true]]]) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'res.partner', 'search_count', array(array(array('is_company', '=', true)))); - - .. code-tab:: java - - (Integer)models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "search_count", - asList(asList( - asList("is_company", "=", true))) - )); - - .. code-tab:: go - - var counter int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "search_count", - []any{[]any{ - []any{"is_company", "=", true}, - }}, - }, &counter); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - 19 - -.. note:: - Calling ``search`` then ``search_count`` (or the other way around) may not - yield coherent results if other users are using the server: stored data - could have changed between the calls. - -Read records ------------- - -Record data are accessible via the :meth:`~odoo.models.Model.read` method, -which takes a list of ids (as returned by -:meth:`~odoo.models.Model.search`), and optionally a list of fields to -fetch. By default, it fetches all the fields the current user can read, -which tends to be a huge amount. - -.. example:: - - .. tabs:: - - .. code-tab:: python - - ids = models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]], {'limit': 1}) - [record] = models.execute_kw(db, uid, password, 'res.partner', 'read', [ids]) - # count the number of fields fetched by default - len(record) - - .. code-tab:: ruby - - ids = models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', true]]], {limit: 1}) - record = models.execute_kw(db, uid, password, 'res.partner', 'read', [ids]).first - # count the number of fields fetched by default - record.length - - .. code-tab:: php - - $ids = $models->execute_kw($db, $uid, $password, 'res.partner', 'search', array(array(array('is_company', '=', true))), array('limit'=>1)); - $records = $models->execute_kw($db, $uid, $password, 'res.partner', 'read', array($ids)); - // count the number of fields fetched by default - count($records[0]); - - .. code-tab:: java - - final List ids = asList((Object[])models.execute( - "execute_kw", asList( - db, uid, password, - "res.partner", "search", - asList(asList( - asList("is_company", "=", true))), - new HashMap() {{ put("limit", 1); }}))); - final Map record = (Map)((Object[])models.execute( - "execute_kw", asList( - db, uid, password, - "res.partner", "read", - asList(ids) - ) - ))[0]; - // count the number of fields fetched by default - record.size(); - - .. code-tab:: go - - var ids []int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "search", - []any{[]any{ - []any{"is_company", "=", true}, - }}, - map[string]int64{"limit": 1}, - }, &ids); err != nil { - log.Fatal(err) - } - var records []any - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "read", - ids, - }, &records); err != nil { - log.Fatal(err) - } - // count the number of fields fetched by default - count := len(records) - - Result: - - .. code-block:: json - - 121 - - Conversely, picking only three fields deemed interesting. - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'read', [ids], {'fields': ['name', 'country_id', 'comment']}) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'read', [ids], {fields: %w(name country_id comment)}) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'res.partner', 'read', array($ids), array('fields'=>array('name', 'country_id', 'comment'))); + res_search = requests.post( + f"{BASE_URL}/res.partner/search", + headers=headers, + json={ + "context": {"lang": "en_US"}, + "domain": [ + ("name", "ilike", "%deco%"), + ("is_company", "=", True), + ], + }, + ) + res_search.raise_for_status() + ids = res_search.json() + + res_read = requests.post( + f"{BASE_URL}/res.partner/read", + headers=headers, + json={ + "ids": ids, + "context": {"lang": "en_US"}, + "fields": ["name"], + } + ) + res_read.raise_for_status() + names = res_read.json() + print(names) - .. code-tab:: java - asList((Object[])models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "read", - asList(ids), - new HashMap() {{ - put("fields", asList("name", "country_id", "comment")); - }} - ))); + .. code-tab:: javascript - .. code-tab:: go + (async () => { + const BASE_URL = "https://mycompany.example.com/json/2"; + const API_KEY = ; // get it from a secure location + const headers = { + "Content-Type": "application/json", + "Authorization": "bearer " + API_KEY, + "X-Odoo-Database": DATABASE, + } - var recordFields []map[string]any - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "read", - ids, - map[string][]string{ - "fields": {"name", "country_id", "comment"}, - }, - }, &recordFields); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - [{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}] + const reqSearch = { + method: "POST", + headers: headers, + body: { + context: {lang: "en_US"}, + domain: [ + ["name", "ilike", "%deco%"], + ["is_company", "=", true], + ], + }, + }; + const resSearch = await fetch(BASE_URL + "/res.partner/search_read", reqSearch); + if (!response.ok) throw new Error(resSearch.json()); + const ids = await resSearch.json(); + + const reqRead = { + method: "POST", + headers: headers, + body: { + ids: ids, + context: {lang: "en_US"}, + fields: ["name"], + }, + }; + const resRead = await fetch(BASE_URL + "/res.partner/search_read", reqRead); + if (!response.ok) throw new Error(resRead.json()); + const names = await resRead.json(); + console.log(names); + })(); + + .. code-tab:: bash + + set -eu + + DATABASE=mycompany + BASE_URL=https://$DATABASE.odoo.com/json/2 + API_KEY= + + ids=$(curl $BASE_URL/res.partner/search \ + -X POST \ + --oauth2-bearer $API_KEY \ + -H "X-Odoo-Database: $DATABASE" \ + -H "Content-Type: application/json" \ + -d '{"context": {"lang": "en_US"}, "domain": [["name", "ilike", "%deco%"], ["is_company", "=", true]]}' \ + --silent \ + --fail + ) + curl $BASE_URL/res.partner/read \ + -X POST \ + --oauth2-bearer $API_KEY \ + -H "X-Odoo-Database: $DATABASE" \ + -H "Content-Type: application/json" \ + -d "{\"ids\": $ids, \"context\": {\"lang\": \"en_US\"}, \"fields\": [\"name\"]}" \ + --silent \ + --fail-with-body + + +The above example would be equivalent to running:: + + Model = self.env["res.partner"].with_context({"lang": "en_US"}) + records = Model.search([("name", "ilike", "%deco%"), ("is_company", "=", True)]) + return json.dumps(records.ids) + +Then in a new transaction:: + + records = self.env["res.partner"].with_context({"lang": "en_US"}).browse(ids) + names = records.read(["name"]) + return json.dumps(names) + + +Dynamic Documentation +===================== + +Under construction + + +Migrating from XML-RPC / JSON-RPC +================================= + +Both the XML-RPC and JSON-RPC APIs at endpoints ``/xmlrpc``, ``/xmlrpc/2`` and ``/jsonrpc`` are +scheduled for removal in Odoo 20 (fall 2026). Both RPC APIs expose the three same services: common, +db (database) and object. All three services are deprecated. .. note:: - Even if the ``id`` field is not requested, it is always returned. - -List record fields ------------------- - -:meth:`~odoo.models.Model.fields_get` can be used to inspect -a model's fields and check which ones seem to be of interest. - -Because it returns a large amount of meta-information (it is also used by client -programs) it should be filtered before printing, the most interesting items -for a human user are ``string`` (the field's label), ``help`` (a help text if -available) and ``type`` (to know which values to expect, or to send when -updating a record). - -.. example:: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [], {'attributes': ['string', 'help', 'type']}) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [], {attributes: %w(string help type)}) - - .. code-tab:: php - $models->execute_kw($db, $uid, $password, 'res.partner', 'fields_get', array(), array('attributes' => array('string', 'help', 'type'))); + The other controllers ``@route(type='jsonrpc')`` (known until Odoo 18 as ``type='json'``) are not + subject to this deprecation notice. - .. code-tab:: java - (Map>)models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "fields_get", - emptyList(), - new HashMap() {{ - put("attributes", asList("string", "help", "type")); - }} - )); +Common service +-------------- - .. code-tab:: go +The common service defines 3 fonctions: - recordFields := map[string]string{} - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "fields_get", - []any{}, - map[string][]string{ - "attributes": {"string", "help", "type"}, - }, - }, &recordFields); err != nil { - log.Fatal(err) - } +1. ``version()`` +2. ``login(db, login, password)`` +3. ``authenticate(db, login, password, user_agent_env)`` - Result: +The version function is replaced by the ``/web/version`` endpoint. - .. code-block:: json +.. code:: http - { - "ean13": { - "type": "char", - "help": "BarCode", - "string": "EAN13" - }, - "property_account_position_id": { - "type": "many2one", - "help": "The fiscal position will determine taxes and accounts used for the partner.", - "string": "Fiscal Position" - }, - "signup_valid": { - "type": "boolean", - "help": "", - "string": "Signup Token is Valid" - }, - "date_localization": { - "type": "date", - "help": "", - "string": "Geo Localization Date" - }, - "ref_company_ids": { - "type": "one2many", - "help": "", - "string": "Companies that refers to partner" - }, - "sale_order_count": { - "type": "integer", - "help": "", - "string": "# of Sales Order" - }, - "purchase_order_count": { - "type": "integer", - "help": "", - "string": "# of Purchase Order" - }, + GET /web/version HTTP/1.1 -Search and read ---------------- +.. code:: http -Because it is a very common task, Odoo provides a -:meth:`~odoo.models.Model.search_read` shortcut which, as its name suggests, is -equivalent to a :meth:`~odoo.models.Model.search` followed by a -:meth:`~odoo.models.Model.read`, but avoids having to perform two requests -and keep ids around. + HTTP/1.1 200 OK + Content-Type: application/json -Its arguments are similar to :meth:`~odoo.models.Model.search`'s, but it -can also take a list of ``fields`` (like :meth:`~odoo.models.Model.read`, -if that list is not provided it will fetch all fields of matched records). + {"version_info": [19, 0, 0, "final", 0, ""], "version": "19.0"} -.. example:: +The two ``login`` and ``authenticate`` functions return the user id corresponding to the user after +a successful login. The user id and password are necessary for subsequent RPC calls to the *object* +service. The JSON-2 API uses a different authentication scheme where neither the user id nor the +password are used. It is still possible to get the user own id doing a JSON-2 request for +``res.users/context_get`` with no ids (the current user is extracted from the API key). - .. tabs:: +Database service +---------------- - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'search_read', [[['is_company', '=', True]]], {'fields': ['name', 'country_id', 'comment'], 'limit': 5}) +.. seealso:: - .. code-tab:: ruby + :ref:`db_manager_security` - models.execute_kw(db, uid, password, 'res.partner', 'search_read', [[['is_company', '=', true]]], {fields: %w(name country_id comment), limit: 5}) +The db service defines 13 fonctions: - .. code-tab:: php +#. ``create_database(master_pwd, db_name, demo, lang, user_password, login, country_code, phone)`` +#. ``duplicate_database(master_pwd, db_original_name, db_name, neutralize_database)`` +#. ``drop(master_pwd, db_name)`` +#. ``dump(master_pwd, db_name, format)`` +#. ``restore(master_pwd, db_name, data, copy)`` +#. ``change_admin_password(master_pwd, new_password)`` +#. ``rename(master_pwd, old_name, new_name)`` +#. ``migrate_databases(master_pwd, databases)`` +#. ``db_exist(db_name)`` +#. ``list()`` +#. ``list_lang()`` +#. ``list_countries(master_pwd)`` +#. ``server_version()`` - $models->execute_kw($db, $uid, $password, 'res.partner', 'search_read', array(array(array('is_company', '=', true))), array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5)); +Many of those function are accessible via the ``/web/database`` controllers. Those controllers +work hand-in-hand with the HTML form at ``/web/database/manager`` and are accessible via HTTP. - .. code-tab:: java +The following controllers use verb ``POST`` and content-type ``application/x-www-form-urlencoded``. - asList((Object[])models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "search_read", - asList(asList( - asList("is_company", "=", true))), - new HashMap() {{ - put("fields", asList("name", "country_id", "comment")); - put("limit", 5); - }} - ))); +#. ``/web/database/create``, takes inputs ``master_pwd``, ``name``, ``login``, ``password``, + ``demo``, ``lang`` and ``phone``. +#. ``/web/database/duplicate``, takes inputs ``master_pwd``, ``name``, ``new_name`` and + ``neutralize_database`` (not neutralized by default). +#. ``/web/database/drop``, takes inputs ``master_pwd`` and ``name``. +#. ``/web/database/backup``, takes inputs ``master_pwd``, ``name`` and ``backup_format`` (zip by + default), returns the backup in the http response. +#. ``/web/database/change_password``, takes inputs ``master_pwd`` and ``master_pwd_new``. - .. code-tab:: go +The following controller use verb ``POST`` and content-type ``multipart/form-data``. - var recordFields []map[string]any - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "search_read", - []any{[]any{ - []any{"is_company", "=", true}, - }}, - map[string]any{ - "fields": []string{"name", "country_id", "comment"}, - "limit": 5, - }, - }, &recordFields); err != nil { - log.Fatal(err) - } +* ``/web/database/restore``, takes inputs ``master_pwd``, ``name``, ``copy`` (not copied by + default) and ``neutralize`` (not neutralized by default), it takes a file input ``backup_file``. - Result: +The following controller use verb ``POST`` and content-type ``application/json-rpc``. - .. code-block:: json +* ``/web/database/list``, takes an empty json object as input, returns the database list under the + json response's ``result`` entry. - [ - { - "comment": false, - "country_id": [ 21, "Belgium" ], - "id": 7, - "name": "Agrolait" - }, - { - "comment": false, - "country_id": [ 76, "France" ], - "id": 18, - "name": "Axelor" - }, - { - "comment": false, - "country_id": [ 233, "United Kingdom" ], - "id": 12, - "name": "Bank Wealthy and sons" - }, - { - "comment": false, - "country_id": [ 105, "India" ], - "id": 14, - "name": "Best Designers" - }, - { - "comment": false, - "country_id": [ 76, "France" ], - "id": 17, - "name": "Camptocamp" - } - ] +The remaining function are: ``server_version``, which exists under ``/web/version``; ``list_lang`` +and ``list_countries`` which exist via JSON-2 on the ``res.lang`` and ``res.country`` models; and +``migrate_databases`` which as no programmable API at the moment. -Create records +Object service -------------- -Records of a model are created using :meth:`~odoo.models.Model.create`. The -method creates a single record and returns its database identifier. - -:meth:`~odoo.models.Model.create` takes a mapping of fields to values, used -to initialize the record. For any field which has a default value and is not -set through the mapping argument, the default value will be used. +The object service defines 2 fonctions: -.. example:: +#. ``execute(db, uid, passwd, model, method, *args)`` +#. ``execute_kw(db, uid, passwd, model, method, args, kw={})`` - .. tabs:: +They both allow for access to all public model methods, including the generic ORM ones. - .. code-tab:: python +Both functions are stateless. It means that the database, user id and user password are to be +provided for each call. The model, method are arguments must likewas be provided. The ``execute`` +function takes as many extra positional arguments as necessary. The ``execute_kw`` function takes a +``args`` list of positional arguments and an optional ``kw`` dict of keyword arguments. - id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{'name': "New Partner"}]) +The records ids are extracted from the first ``args``. When the called method is decorated with +``@api.model``, no record ids are extracted and ``args`` is left as-is. It is only possible to give +a context with ``execute_kw`` as it is extracted from the keyword argument named ``context``. - .. code-tab:: ruby +Example, to run the following: - id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{name: "New Partner"}]) +.. code:: python - .. code-tab:: php + (env['res.partner'] + .with_user(2) # admin + .with_context(lang='en_US') + .browse([1, 2, 3]) + .read(['name'], load=None) + ) - $id = $models->execute_kw($db, $uid, $password, 'res.partner', 'create', array(array('name'=>"New Partner"))); +Using XML-RPC (JSON-RPC would be similar): - .. code-tab:: java +.. code:: python - final Integer id = (Integer)models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "create", - asList(new HashMap() {{ put("name", "New Partner"); }}) - )); + from xmlrpc.client import ServerProxy + object = ServerProxy(...) + ids = [1, 2, 3] + fields = ['name'] + load = None - .. code-tab:: go + object.execute("database", 2, "admin", "res.partner", "read", ids, fields, load) + object.execute("database", 2, "admin", "res.partner", "search", [ + ids, + fields, + ], { + "context": {"lang": "en_US"}, + "load": load, + }) - var id int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "create", - []map[string]string{ - {"name": "New Partner"}, - }, - }, &id); err != nil { - log.Fatal(err) - } +The JSON-2 API replaces the object service with a few differences. The database must only be +provided (via the ``X-Odoo-Database`` HTTP header) on systems where there are multiple databases +available for a same domain. The login/password authentication scheme is replaced by an API key (via +the ``Authorization: bearer`` HTTP header). The ``model`` and ``method`` are placed in the URL. The +request body is a JSON object with all the methods arguments, plus ``ids`` and ``context``. All +the arguments are named, there is no way in JSON-2 to call a function with positional arguments. - Result: +Using JSON-2: - .. code-block:: json +.. code:: python - 78 - -.. warning:: - While most value types are what would expect (integer for - :class:`~odoo.fields.Integer`, string for :class:`~odoo.fields.Char` - or :class:`~odoo.fields.Text`), - - - :class:`~odoo.fields.Date`, :class:`~odoo.fields.Datetime` and - :class:`~odoo.fields.Binary` fields use string values - - :class:`~odoo.fields.One2many` and :class:`~odoo.fields.Many2many` - use a special command protocol detailed in :meth:`the documentation to - the write method `. - -Update records --------------- - -Records can be updated using :meth:`~odoo.models.Model.write`. It takes -a list of records to update and a mapping of updated fields to values similar -to :meth:`~odoo.models.Model.create`. - -Multiple records can be updated simultaneously, but they will all get the same -values for the fields being set. It is not possible to perform -"computed" updates (where the value being set depends on an existing value of -a record). - -.. example:: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {'name': "Newer partner"}]) - # get record name after having changed it - models.execute_kw(db, uid, password, 'res.partner', 'read', [[id], ['display_name']]) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {name: "Newer partner"}]) - # get record name after having changed it - models.execute_kw(db, uid, password, 'res.partner', 'read', [[id], ['display_name']]) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'res.partner', 'write', array(array($id), array('name'=>"Newer partner"))); - // get record name after having changed it - $models->execute_kw($db, $uid, $password, - 'res.partner', 'read', array(array($id), array('display_name'))); - - .. code-tab:: java - - models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "write", - asList( - asList(id), - new HashMap() {{ put("name", "Newer Partner"); }} - ) - )); - // get record name after having changed it - asList((Object[])models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "read", - asList(asList(id), asList("display_name")) - ))); - - .. code-tab:: go - - var result bool - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "write", - []any{ - []int64{id}, - map[string]string{"name": "Newer partner"}, - }, - }, &result); err != nil { - log.Fatal(err) - } - // get record name after having changed it - var record []any - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "name_get", - []any{ - []int64{id}, - }, - }, &record); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - [[78, "Newer partner"]] - -Delete records --------------- + import requests -Records can be deleted in bulk by providing their ids to -:meth:`~odoo.models.Model.unlink`. - -.. example:: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]]) - # check if the deleted record is still in the database - models.execute_kw(db, uid, password, 'res.partner', 'search', [[['id', '=', id]]]) - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]]) - # check if the deleted record is still in the database - models.execute_kw(db, uid, password, 'res.partner', 'search', [[['id', '=', id]]]) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'res.partner', 'unlink', array(array($id))); - // check if the deleted record is still in the database - $models->execute_kw( - $db, $uid, $password, 'res.partner', 'search', array(array(array('id', '=', $id))) - ); - - .. code-tab:: java - - models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "unlink", - asList(asList(id)))); - // check if the deleted record is still in the database - asList((Object[])models.execute("execute_kw", asList( - db, uid, password, - "res.partner", "search", - asList(asList(asList("id", "=", 78))) - ))); - - .. code-tab:: go - - var result bool - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "unlink", - []any{ - []int64{id}, - }, - }, &result); err != nil { - log.Fatal(err) - } - // check if the deleted record is still in the database - var record []any - if err := models.Call("execute_kw", []any{ - db, uid, password, - "res.partner", "search", - []any{[]any{ - []any{"id", "=", id}, - }}, - }, &record); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - [] - -Inspection and introspection ----------------------------- - -While we previously used :meth:`~odoo.models.Model.fields_get` to query a -model and have been using an arbitrary model from the start, Odoo stores -most model metadata inside a few meta-models which allow both querying the -system and altering models and fields (with some limitations) on the fly over -XML-RPC. - -.. _reference/webservice/inspection/models: - -``ir.model`` -~~~~~~~~~~~~ - -Provides information about Odoo models via its various fields. - -``name`` - a human-readable description of the model -``model`` - the name of each model in the system -``state`` - whether the model was generated in Python code (``base``) or by creating - an ``ir.model`` record (``manual``) -``field_id`` - list of the model's fields through a :class:`~odoo.fields.One2many` to - :ref:`reference/webservice/inspection/fields` -``view_ids`` - :class:`~odoo.fields.One2many` to the :doc:`../reference/user_interface/view_architectures` - defined for the model -``access_ids`` - :class:`~odoo.fields.One2many` relation to the - :ref:`reference/security/acl` set on the model - -``ir.model`` can be used to - -- Query the system for installed models (as a precondition to operations - on the model or to explore the system's content). -- Get information about a specific model (generally by listing the fields - associated with it). -- Create new models dynamically over RPC. - -.. important:: - * Custom model names must start with ``x_``. - * The ``state`` must be provided and set to ``manual``, otherwise the model will - not be loaded. - * It is not possible to add new *methods* to a custom model, only fields. - -.. example:: - - A custom model will initially contain only the "built-in" fields available - on all models: - - .. tabs:: - - .. code-tab:: python - - models.execute_kw(db, uid, password, 'ir.model', 'create', [{ - 'name': "Custom Model", - 'model': "x_custom_model", - 'state': 'manual', - }]) - models.execute_kw(db, uid, password, 'x_custom_model', 'fields_get', [], {'attributes': ['string', 'help', 'type']}) - - .. code-tab:: php - - $models->execute_kw($db, $uid, $password, 'ir.model', 'create', array(array( - 'name' => "Custom Model", - 'model' => 'x_custom_model', - 'state' => 'manual' - ))); - $models->execute_kw($db, $uid, $password, 'x_custom_model', 'fields_get', array(), array('attributes' => array('string', 'help', 'type'))); - - .. code-tab:: ruby - - models.execute_kw(db, uid, password, 'ir.model', 'create', [{ - name: "Custom Model", - model: 'x_custom_model', - state: 'manual' - }]) - fields = models.execute_kw(db, uid, password, 'x_custom_model', 'fields_get', [], {attributes: %w(string help type)}) - - .. code-tab:: java - - models.execute( - "execute_kw", asList( - db, uid, password, - "ir.model", "create", - asList(new HashMap() {{ - put("name", "Custom Model"); - put("model", "x_custom_model"); - put("state", "manual"); - }}) - )); - final Object fields = models.execute( - "execute_kw", asList( - db, uid, password, - "x_custom_model", "fields_get", - emptyList(), - new HashMap () {{ - put("attributes", asList( - "string", - "help", - "type")); - }} - )); - - .. code-tab:: go - - var id int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "ir.model", "create", - []map[string]string{ - { - "name": "Custom Model", - "model": "x_custom_model", - "state": "manual", - }, - }, - }, &id); err != nil { - log.Fatal(err) - } - recordFields := map[string]string{} - if err := models.Call("execute_kw", []any{ - db, uid, password, - "x_custom_model", "fields_get", - []any{}, - map[string][]string{ - "attributes": {"string", "help", "type"}, - }, - }, &recordFields); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json + DATABSE = ... + DOMAIN = ... + API_KEY = "6578616d706c65206a736f6e20617069206b6579" - { - "create_uid": { - "type": "many2one", - "string": "Created by" - }, - "create_date": { - "type": "datetime", - "string": "Created on" - }, - "__last_update": { - "type": "datetime", - "string": "Last Modified on" - }, - "write_uid": { - "type": "many2one", - "string": "Last Updated by" - }, - "write_date": { - "type": "datetime", - "string": "Last Updated on" - }, - "display_name": { - "type": "char", - "string": "Display Name" - }, - "id": { - "type": "integer", - "string": "Id" - } - } - -.. _reference/webservice/inspection/fields: - -``ir.model.fields`` -~~~~~~~~~~~~~~~~~~~ - -Provides information about the fields of Odoo models and allows adding -custom fields without using Python code. - -``model_id`` - :class:`~odoo.fields.Many2one` to - :ref:`reference/webservice/inspection/models` to which the field belongs -``name`` - the field's technical name (used in ``read`` or ``write``) -``field_description`` - the field's user-readable label (e.g. ``string`` in ``fields_get``) -``ttype`` - the :ref:`type ` of field to create -``state`` - whether the field was created via Python code (``base``) or via - ``ir.model.fields`` (``manual``) -``required``, ``readonly``, ``translate`` - enables the corresponding flag on the field -``groups`` - :ref:`field-level access control `, a - :class:`~odoo.fields.Many2many` to ``res.groups`` -``selection``, ``size``, ``on_delete``, ``relation``, ``relation_field``, ``domain`` - type-specific properties and customizations, see :ref:`the fields - documentation ` for details - -.. important:: - - Like custom models, only new fields created with ``state="manual"`` are activated as actual - fields on the model. - - Computed fields can not be added via ``ir.model.fields``, some field meta-information - (defaults, onchange) can not be set either. - -.. example:: - - .. tabs:: - - .. code-tab:: python - - id = models.execute_kw(db, uid, password, 'ir.model', 'create', [{ - 'name': "Custom Model", - 'model': "x_custom", - 'state': 'manual', - }]) - models.execute_kw(db, uid, password, 'ir.model.fields', 'create', [{ - 'model_id': id, - 'name': 'x_name', - 'ttype': 'char', - 'state': 'manual', - 'required': True, - }]) - record_id = models.execute_kw(db, uid, password, 'x_custom', 'create', [{'x_name': "test record"}]) - models.execute_kw(db, uid, password, 'x_custom', 'read', [[record_id]]) - - .. code-tab:: php - - $id = $models->execute_kw($db, $uid, $password, 'ir.model', 'create', array(array( - 'name' => "Custom Model", - 'model' => 'x_custom', - 'state' => 'manual' - ))); - $models->execute_kw($db, $uid, $password, 'ir.model.fields', 'create', array(array( - 'model_id' => $id, - 'name' => 'x_name', - 'ttype' => 'char', - 'state' => 'manual', - 'required' => true - ))); - $record_id = $models->execute_kw($db, $uid, $password, 'x_custom', 'create', array(array('x_name' => "test record"))); - $models->execute_kw($db, $uid, $password, 'x_custom', 'read', array(array($record_id))); - - .. code-tab:: ruby - - id = models.execute_kw(db, uid, password, 'ir.model', 'create', [{ - name: "Custom Model", - model: "x_custom", - state: 'manual' - }]) - models.execute_kw(db, uid, password, 'ir.model.fields', 'create', [{ - model_id: id, - name: "x_name", - ttype: "char", - state: "manual", - required: true - }]) - record_id = models.execute_kw(db, uid, password, 'x_custom', 'create', [{x_name: "test record"}]) - models.execute_kw(db, uid, password, 'x_custom', 'read', [[record_id]]) - - .. code-tab:: java - - final Integer id = (Integer)models.execute( - "execute_kw", asList( - db, uid, password, - "ir.model", "create", - asList(new HashMap() {{ - put("name", "Custom Model"); - put("model", "x_custom"); - put("state", "manual"); - }}) - )); - models.execute( - "execute_kw", asList( - db, uid, password, - "ir.model.fields", "create", - asList(new HashMap() {{ - put("model_id", id); - put("name", "x_name"); - put("ttype", "char"); - put("state", "manual"); - put("required", true); - }}) - )); - final Integer record_id = (Integer)models.execute( - "execute_kw", asList( - db, uid, password, - "x_custom", "create", - asList(new HashMap() {{ - put("x_name", "test record"); - }}) - )); - - client.execute( - "execute_kw", asList( - db, uid, password, - "x_custom", "read", - asList(asList(record_id)) - )); - - .. code-tab:: go - - var id int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "ir.model", "create", - []map[string]string{ - { - "name": "Custom Model", - "model": "x_custom", - "state": "manual", - }, - }, - }, &id); err != nil { - log.Fatal(err) - } - var fieldId int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "ir.model.fields", "create", - []map[string]any{ - { - "model_id": id, - "name": "x_name", - "ttype": "char", - "state": "manual", - "required": true, - }, - }, - }, &fieldId); err != nil { - log.Fatal(err) - } - var recordId int64 - if err := models.Call("execute_kw", []any{ - db, uid, password, - "x_custom", "create", - []map[string]string{ - {"x_name": "test record"}, - }, - }, &recordId); err != nil { - log.Fatal(err) - } - var recordFields []map[string]any - if err := models.Call("execute_kw", []any{ - db, uid, password, - "x_custom", "read", - [][]int64{{recordId}}, - }, recordFields); err != nil { - log.Fatal(err) - } - - Result: - - .. code-block:: json - - [ - { - "create_uid": [1, "Administrator"], - "x_name": "test record", - "__last_update": "2014-11-12 16:32:13", - "write_uid": [1, "Administrator"], - "write_date": "2014-11-12 16:32:13", - "create_date": "2014-11-12 16:32:13", - "id": 1, - "display_name": "test record" - } - ] + requests.post( + f"https://{DOMAIN}/json/2/res.partner/read", + headers={ + # "X-Odoo-Database": DATABASE, # only when DOMAIN isn't enough + "Authorization": f"bearer {API_KEY}", + }, + json={ + "ids": [1, 2, 3], + "context": {"lang": "en_US"}, + "fields": ["name"], + "load": None, + }, + ).json() -.. _PostgreSQL: https://www.postgresql.org -.. _XML-RPC: https://en.wikipedia.org/wiki/XML-RPC -.. _base64: https://en.wikipedia.org/wiki/Base64 diff --git a/content/developer/reference/external_api/account-security2.png b/content/developer/reference/external_api/account-security2.png new file mode 100644 index 0000000000..e7333bc2d9 Binary files /dev/null and b/content/developer/reference/external_api/account-security2.png differ diff --git a/content/developer/reference/external_api/new-api-key.png b/content/developer/reference/external_api/new-api-key.png new file mode 100644 index 0000000000..cb77f6db6f Binary files /dev/null and b/content/developer/reference/external_api/new-api-key.png differ diff --git a/content/developer/reference/external_api/preferences2.png b/content/developer/reference/external_api/preferences2.png new file mode 100644 index 0000000000..35698dd6b2 Binary files /dev/null and b/content/developer/reference/external_api/preferences2.png differ diff --git a/content/developer/reference/external_rpc_api.rst b/content/developer/reference/external_rpc_api.rst new file mode 100644 index 0000000000..c84fb603cd --- /dev/null +++ b/content/developer/reference/external_rpc_api.rst @@ -0,0 +1,1467 @@ +================ +External RPC API +================ + +.. danger:: + + .. deprecated:: 19.0 + + Both the XML-RPC and JSON-RPC APIs at endpoints ``/xmlrpc``, ``/xmlrpc/2`` and ``/jsonrpc`` are + scheduled for removal in Odoo 20 (fall 2026). The :doc:`external_api` acts as a replacement. + + The other controllers ``@route(type='jsonrpc')`` (known until Odoo 18 as ``type='json'``) are not + subject to this deprecation notice. + +Odoo is usually extended internally via modules, but many of its features and +all of its data are also available from the outside for external analysis or +integration with various tools. Part of the :ref:`reference/orm/model` API is +easily available over XML-RPC_ and accessible from a variety of languages. + +Starting with PHP8, the XML-RPC extension may not be available by default. +Check out the `manual `_ +for the installation steps. + +Access to data via the external API is only available on *Custom* Odoo pricing plans. Access to +the external API is not available on *One App Free* or *Standard* plans. For more information +visit the `Odoo pricing page `_ or reach out to your Customer +Success Manager. + +Connection +========== + +Configuration +------------- + +If you already have an Odoo server installed, you can just use its parameters. + +.. important:: + + For Odoo Online instances (.odoo.com), users are created without a + *local* password (as a person you are logged in via the Odoo Online + authentication system, not by the instance itself). To use XML-RPC on Odoo + Online instances, you will need to set a password on the user account you + want to use: + + * Log in your instance with an administrator account. + * Go to :menuselection:`Settings --> Users & Companies --> Users`. + * Click on the user you want to use for XML-RPC access. + * Click on :guilabel:`Action` and select :guilabel:`Change Password`. + * Set a :guilabel:`New Password` value then click :guilabel:`Change Password`. + + The *server url* is the instance's domain (e.g. + *https://mycompany.odoo.com*), the *database name* is the name of the + instance (e.g. *mycompany*). The *username* is the configured user's login + as shown by the *Change Password* screen. + +.. tabs:: + + .. code-tab:: python + + url = + db = + username = 'admin' + password = + + .. code-tab:: ruby + + url = + db = + username = "admin" + password = + + .. code-tab:: php + + $url = ; + $db = ; + $username = "admin"; + $password = ; + + .. code-tab:: java + + final String url = , + db = , + username = "admin", + password = ; + + .. code-tab:: go + + var ( + url = + db = + username = "admin" + password = + ) + +.. _api/external_api/keys: + +API Keys +~~~~~~~~ + +.. versionadded:: 14.0 + +Odoo has support for **api keys** and (depending on modules or settings) may +**require** these keys to perform webservice operations. + +The way to use API Keys in your scripts is to simply replace your **password** +by the key. The login remains in-use. You should store the API Key as carefully +as the password as they essentially provide the same access to your user +account (although they can not be used to log-in via the interface). + +In order to add a key to your account, simply go to your +:guilabel:`Preferences` (or :guilabel:`My Profile`): + +.. image:: external_api/preferences.png + :align: center + +then open the :guilabel:`Account Security` tab, and click +:guilabel:`New API Key`: + +.. image:: external_api/account-security.png + :align: center + +Input a description for the key, **this description should be as clear and +complete as possible**: it is the only way you will have to identify your keys +later and know whether you should remove them or keep them around. + +Click :guilabel:`Generate Key`, then copy the key provided. **Store this key +carefully**: it is equivalent to your password, and just like your password +the system will not be able to retrieve or show the key again later on. If you lose +this key, you will have to create a new one (and probably delete the one you +lost). + +Once you have keys configured on your account, they will appear above the +:guilabel:`New API Key` button, and you will be able to delete them: + +.. image:: external_api/delete-key.png + :align: center + +**A deleted API key can not be undeleted or re-set**. You will have to generate +a new key and update all the places where you used the old one. + +Test database +~~~~~~~~~~~~~ + +To make exploration simpler, you can also ask https://demo.odoo.com for a test +database: + +.. tabs:: + + .. code-tab:: python + + import xmlrpc.client + info = xmlrpc.client.ServerProxy('https://demo.odoo.com/start').start() + url, db, username, password = info['host'], info['database'], info['user'], info['password'] + + .. code-tab:: ruby + + require "xmlrpc/client" + info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start') + url, db, username, password = info['host'], info['database'], info['user'], info['password'] + + .. group-tab:: PHP + + .. code-block:: php + + require_once('ripcord.php'); + $info = ripcord::client('https://demo.odoo.com/start')->start(); + list($url, $db, $username, $password) = array($info['host'], $info['database'], $info['user'], $info['password']); + + .. note:: + These examples use the `Ripcord `_ + library, which provides a simple XML-RPC API. Ripcord requires that + `XML-RPC support be enabled + `_ in your PHP + installation. + + Since calls are performed over + `HTTPS `_, it also requires that + the `OpenSSL extension + `_ be enabled. + + .. group-tab:: Java + + .. code-block:: java + + final XmlRpcClient client = new XmlRpcClient(); + + final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl(); + start_config.setServerURL(new URL("https://demo.odoo.com/start")); + final Map info = (Map)client.execute( + start_config, "start", emptyList()); + + final String url = info.get("host"), + db = info.get("database"), + username = info.get("user"), + password = info.get("password"); + + .. note:: + These examples use the `Apache XML-RPC library `_. + + The examples do not include imports as these imports couldn't be + pasted in the code. + + .. group-tab:: Go + + .. code-block:: go + + client, err := xmlrpc.NewClient("https://demo.odoo.com/start", nil) + if err != nil { + log.Fatal(err) + } + info := map[string]string{} + client.Call("start", nil, &info) + url = info["host"].(string) + db = info["database"].(string) + username = info["user"].(string) + password = info["password"].(string) + + .. note:: + These examples use the `github.com/kolo/xmlrpc library `_. + + The examples do not include imports as these imports couldn't be + pasted in the code. + +Logging in +---------- + +Odoo requires users of the API to be authenticated before they can query most +data. + +The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require +authentication, such as the authentication itself or fetching version +information. To verify if the connection information is correct before trying +to authenticate, the simplest call is to ask for the server's version. The +authentication itself is done through the ``authenticate`` function and +returns a user identifier (``uid``) used in authenticated calls instead of +the login. + +.. tabs:: + + .. code-tab:: python + + common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url)) + common.version() + + .. code-tab:: ruby + + common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common") + common.call('version') + + .. code-tab:: php + + $common = ripcord::client("$url/xmlrpc/2/common"); + $common->version(); + + .. code-tab:: java + + final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl(); + common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url))); + client.execute(common_config, "version", emptyList()); + + .. code-tab:: go + + client, err := xmlrpc.NewClient(fmt.Sprintf("%s/xmlrpc/2/common", url), nil) + if err != nil { + log.Fatal(err) + } + common := map[string]any{} + if err := client.Call("version", nil, &common); err != nil { + log.Fatal(err) + } + +Result: + +.. code-block:: json + + { + "server_version": "13.0", + "server_version_info": [13, 0, 0, "final", 0], + "server_serie": "13.0", + "protocol_version": 1, + } + + +.. tabs:: + + .. code-tab:: python + + uid = common.authenticate(db, username, password, {}) + + .. code-tab:: ruby + + uid = common.call('authenticate', db, username, password, {}) + + .. code-tab:: php + + $uid = $common->authenticate($db, $username, $password, array()); + + .. code-tab:: java + + int uid = (int)client.execute(common_config, "authenticate", asList(db, username, password, emptyMap())); + + .. code-tab:: go + + var uid int64 + if err := client.Call("authenticate", []any{ + db, username, password, + map[string]any{}, + }, &uid); err != nil { + log.Fatal(err) + } + +.. _api/external_api/calling_methods: + +Calling methods +=============== + +The second endpoint is ``xmlrpc/2/object``. It is used to call methods of odoo +models via the ``execute_kw`` RPC function. + +Each call to ``execute_kw`` takes the following parameters: + +* the database to use, a string +* the user id (retrieved through ``authenticate``), an integer +* the user's password, a string +* the model name, a string +* the method name, a string +* an array/list of parameters passed by position +* a mapping/dict of parameters to pass by keyword (optional) + +.. example:: + + For instance, to search for records in the ``res.partner`` model, we can call + ``name_search`` with ``name`` passed by position and ``limit`` passed by + keyword (in order to get maximum 10 results): + + .. tabs:: + + .. code-tab:: python + + models = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url)) + models.execute_kw(db, uid, password, 'res.partner', 'name_search', ['foo'], {'limit': 10}) + + .. code-tab:: ruby + + models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy + models.execute_kw(db, uid, password, 'res.partner', 'name_search', ['foo'], {limit: 10}) + + .. code-tab:: php + + $models = ripcord::client("$url/xmlrpc/2/object"); + $models->execute_kw($db, $uid, $password, 'res.partner', 'name_search', array('foo'), array('limit' => 10)); + + .. code-tab:: java + + final XmlRpcClient models = new XmlRpcClient() {{ + setConfig(new XmlRpcClientConfigImpl() {{ + setServerURL(new URL(String.format("%s/xmlrpc/2/object", url))); + }}); + }}; + models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "name_search", + asList("foo"), + new HashMap() {{ put("limit", 10); }} + )); + + .. code-tab:: go + + models, err := xmlrpc.NewClient(fmt.Sprintf("%s/xmlrpc/2/object", url), nil) + if err != nil { + log.Fatal(err) + } + var result bool + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "name_search", + []string{"foo"}, + map[string]bool{"limit": 10}, + }, &result); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + true + +List records +------------ + +Records can be listed and filtered via :meth:`~odoo.models.Model.search`. + +:meth:`~odoo.models.Model.search` takes a mandatory +:ref:`domain ` filter (possibly empty), and returns the +database identifiers of all records matching the filter. + +.. example:: + + To list customer companies, for instance: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]]) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', true]]]) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'search', array(array(array('is_company', '=', true)))); + + .. code-tab:: java + + asList((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "search", + asList(asList( + asList("is_company", "=", true))) + ))); + + .. code-tab:: go + + var records []int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "search", + []any{[]any{ + []any{"is_company", "=", true}, + }}, + }, &records); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74] + +Pagination +~~~~~~~~~~ + +By default a search will return the ids of all records matching the +condition, which may be a huge number. ``offset`` and ``limit`` parameters are +available to only retrieve a subset of all matched records. + +.. example:: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]], {'offset': 10, 'limit': 5}) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', true]]], {offset: 10, limit: 5}) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'search', array(array(array('is_company', '=', true))), array('offset'=>10, 'limit'=>5)); + + .. code-tab:: java + + asList((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "search", + asList(asList( + asList("is_company", "=", true))), + new HashMap() {{ put("offset", 10); put("limit", 5); }} + ))); + + .. code-tab:: go + + var records []int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "search", + []any{[]any{ + []any{"is_company", "=", true}, + }}, + map[string]int64{"offset": 10, "limit": 5}, + }, &records); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [13, 20, 30, 22, 29] + +Count records +------------- + +Rather than retrieve a possibly gigantic list of records and count them, +:meth:`~odoo.models.Model.search_count` can be used to retrieve +only the number of records matching the query. It takes the same +:ref:`domain ` filter as +:meth:`~odoo.models.Model.search` and no other parameter. + +.. example:: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'search_count', [[['is_company', '=', True]]]) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'search_count', [[['is_company', '=', true]]]) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'search_count', array(array(array('is_company', '=', true)))); + + .. code-tab:: java + + (Integer)models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "search_count", + asList(asList( + asList("is_company", "=", true))) + )); + + .. code-tab:: go + + var counter int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "search_count", + []any{[]any{ + []any{"is_company", "=", true}, + }}, + }, &counter); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + 19 + +.. note:: + Calling ``search`` then ``search_count`` (or the other way around) may not + yield coherent results if other users are using the server: stored data + could have changed between the calls. + +Read records +------------ + +Record data are accessible via the :meth:`~odoo.models.Model.read` method, +which takes a list of ids (as returned by +:meth:`~odoo.models.Model.search`), and optionally a list of fields to +fetch. By default, it fetches all the fields the current user can read, +which tends to be a huge amount. + +.. example:: + + .. tabs:: + + .. code-tab:: python + + ids = models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]], {'limit': 1}) + [record] = models.execute_kw(db, uid, password, 'res.partner', 'read', [ids]) + # count the number of fields fetched by default + len(record) + + .. code-tab:: ruby + + ids = models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', true]]], {limit: 1}) + record = models.execute_kw(db, uid, password, 'res.partner', 'read', [ids]).first + # count the number of fields fetched by default + record.length + + .. code-tab:: php + + $ids = $models->execute_kw($db, $uid, $password, 'res.partner', 'search', array(array(array('is_company', '=', true))), array('limit'=>1)); + $records = $models->execute_kw($db, $uid, $password, 'res.partner', 'read', array($ids)); + // count the number of fields fetched by default + count($records[0]); + + .. code-tab:: java + + final List ids = asList((Object[])models.execute( + "execute_kw", asList( + db, uid, password, + "res.partner", "search", + asList(asList( + asList("is_company", "=", true))), + new HashMap() {{ put("limit", 1); }}))); + final Map record = (Map)((Object[])models.execute( + "execute_kw", asList( + db, uid, password, + "res.partner", "read", + asList(ids) + ) + ))[0]; + // count the number of fields fetched by default + record.size(); + + .. code-tab:: go + + var ids []int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "search", + []any{[]any{ + []any{"is_company", "=", true}, + }}, + map[string]int64{"limit": 1}, + }, &ids); err != nil { + log.Fatal(err) + } + var records []any + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "read", + ids, + }, &records); err != nil { + log.Fatal(err) + } + // count the number of fields fetched by default + count := len(records) + + Result: + + .. code-block:: json + + 121 + + Conversely, picking only three fields deemed interesting. + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'read', [ids], {'fields': ['name', 'country_id', 'comment']}) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'read', [ids], {fields: %w(name country_id comment)}) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'read', array($ids), array('fields'=>array('name', 'country_id', 'comment'))); + + .. code-tab:: java + + asList((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "read", + asList(ids), + new HashMap() {{ + put("fields", asList("name", "country_id", "comment")); + }} + ))); + + .. code-tab:: go + + var recordFields []map[string]any + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "read", + ids, + map[string][]string{ + "fields": {"name", "country_id", "comment"}, + }, + }, &recordFields); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}] + +.. note:: + Even if the ``id`` field is not requested, it is always returned. + +List record fields +------------------ + +:meth:`~odoo.models.Model.fields_get` can be used to inspect +a model's fields and check which ones seem to be of interest. + +Because it returns a large amount of meta-information (it is also used by client +programs) it should be filtered before printing, the most interesting items +for a human user are ``string`` (the field's label), ``help`` (a help text if +available) and ``type`` (to know which values to expect, or to send when +updating a record). + +.. example:: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [], {'attributes': ['string', 'help', 'type']}) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [], {attributes: %w(string help type)}) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'fields_get', array(), array('attributes' => array('string', 'help', 'type'))); + + .. code-tab:: java + + (Map>)models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "fields_get", + emptyList(), + new HashMap() {{ + put("attributes", asList("string", "help", "type")); + }} + )); + + .. code-tab:: go + + recordFields := map[string]string{} + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "fields_get", + []any{}, + map[string][]string{ + "attributes": {"string", "help", "type"}, + }, + }, &recordFields); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + { + "ean13": { + "type": "char", + "help": "BarCode", + "string": "EAN13" + }, + "property_account_position_id": { + "type": "many2one", + "help": "The fiscal position will determine taxes and accounts used for the partner.", + "string": "Fiscal Position" + }, + "signup_valid": { + "type": "boolean", + "help": "", + "string": "Signup Token is Valid" + }, + "date_localization": { + "type": "date", + "help": "", + "string": "Geo Localization Date" + }, + "ref_company_ids": { + "type": "one2many", + "help": "", + "string": "Companies that refers to partner" + }, + "sale_order_count": { + "type": "integer", + "help": "", + "string": "# of Sales Order" + }, + "purchase_order_count": { + "type": "integer", + "help": "", + "string": "# of Purchase Order" + }, + +Search and read +--------------- + +Because it is a very common task, Odoo provides a +:meth:`~odoo.models.Model.search_read` shortcut which, as its name suggests, is +equivalent to a :meth:`~odoo.models.Model.search` followed by a +:meth:`~odoo.models.Model.read`, but avoids having to perform two requests +and keep ids around. + +Its arguments are similar to :meth:`~odoo.models.Model.search`'s, but it +can also take a list of ``fields`` (like :meth:`~odoo.models.Model.read`, +if that list is not provided it will fetch all fields of matched records). + +.. example:: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'search_read', [[['is_company', '=', True]]], {'fields': ['name', 'country_id', 'comment'], 'limit': 5}) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'search_read', [[['is_company', '=', true]]], {fields: %w(name country_id comment), limit: 5}) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'search_read', array(array(array('is_company', '=', true))), array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5)); + + .. code-tab:: java + + asList((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "search_read", + asList(asList( + asList("is_company", "=", true))), + new HashMap() {{ + put("fields", asList("name", "country_id", "comment")); + put("limit", 5); + }} + ))); + + .. code-tab:: go + + var recordFields []map[string]any + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "search_read", + []any{[]any{ + []any{"is_company", "=", true}, + }}, + map[string]any{ + "fields": []string{"name", "country_id", "comment"}, + "limit": 5, + }, + }, &recordFields); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [ + { + "comment": false, + "country_id": [ 21, "Belgium" ], + "id": 7, + "name": "Agrolait" + }, + { + "comment": false, + "country_id": [ 76, "France" ], + "id": 18, + "name": "Axelor" + }, + { + "comment": false, + "country_id": [ 233, "United Kingdom" ], + "id": 12, + "name": "Bank Wealthy and sons" + }, + { + "comment": false, + "country_id": [ 105, "India" ], + "id": 14, + "name": "Best Designers" + }, + { + "comment": false, + "country_id": [ 76, "France" ], + "id": 17, + "name": "Camptocamp" + } + ] + +Create records +-------------- + +Records of a model are created using :meth:`~odoo.models.Model.create`. The +method creates a single record and returns its database identifier. + +:meth:`~odoo.models.Model.create` takes a mapping of fields to values, used +to initialize the record. For any field which has a default value and is not +set through the mapping argument, the default value will be used. + +.. example:: + + .. tabs:: + + .. code-tab:: python + + id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{'name': "New Partner"}]) + + .. code-tab:: ruby + + id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{name: "New Partner"}]) + + .. code-tab:: php + + $id = $models->execute_kw($db, $uid, $password, 'res.partner', 'create', array(array('name'=>"New Partner"))); + + .. code-tab:: java + + final Integer id = (Integer)models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "create", + asList(new HashMap() {{ put("name", "New Partner"); }}) + )); + + .. code-tab:: go + + var id int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "create", + []map[string]string{ + {"name": "New Partner"}, + }, + }, &id); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + 78 + +.. warning:: + While most value types are what would expect (integer for + :class:`~odoo.fields.Integer`, string for :class:`~odoo.fields.Char` + or :class:`~odoo.fields.Text`), + + - :class:`~odoo.fields.Date`, :class:`~odoo.fields.Datetime` and + :class:`~odoo.fields.Binary` fields use string values + - :class:`~odoo.fields.One2many` and :class:`~odoo.fields.Many2many` + use a special command protocol detailed in :meth:`the documentation to + the write method `. + +Update records +-------------- + +Records can be updated using :meth:`~odoo.models.Model.write`. It takes +a list of records to update and a mapping of updated fields to values similar +to :meth:`~odoo.models.Model.create`. + +Multiple records can be updated simultaneously, but they will all get the same +values for the fields being set. It is not possible to perform +"computed" updates (where the value being set depends on an existing value of +a record). + +.. example:: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {'name': "Newer partner"}]) + # get record name after having changed it + models.execute_kw(db, uid, password, 'res.partner', 'read', [[id], ['display_name']]) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {name: "Newer partner"}]) + # get record name after having changed it + models.execute_kw(db, uid, password, 'res.partner', 'read', [[id], ['display_name']]) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'write', array(array($id), array('name'=>"Newer partner"))); + // get record name after having changed it + $models->execute_kw($db, $uid, $password, + 'res.partner', 'read', array(array($id), array('display_name'))); + + .. code-tab:: java + + models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "write", + asList( + asList(id), + new HashMap() {{ put("name", "Newer Partner"); }} + ) + )); + // get record name after having changed it + asList((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "read", + asList(asList(id), asList("display_name")) + ))); + + .. code-tab:: go + + var result bool + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "write", + []any{ + []int64{id}, + map[string]string{"name": "Newer partner"}, + }, + }, &result); err != nil { + log.Fatal(err) + } + // get record name after having changed it + var record []any + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "name_get", + []any{ + []int64{id}, + }, + }, &record); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [[78, "Newer partner"]] + +Delete records +-------------- + +Records can be deleted in bulk by providing their ids to +:meth:`~odoo.models.Model.unlink`. + +.. example:: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]]) + # check if the deleted record is still in the database + models.execute_kw(db, uid, password, 'res.partner', 'search', [[['id', '=', id]]]) + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]]) + # check if the deleted record is still in the database + models.execute_kw(db, uid, password, 'res.partner', 'search', [[['id', '=', id]]]) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'res.partner', 'unlink', array(array($id))); + // check if the deleted record is still in the database + $models->execute_kw( + $db, $uid, $password, 'res.partner', 'search', array(array(array('id', '=', $id))) + ); + + .. code-tab:: java + + models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "unlink", + asList(asList(id)))); + // check if the deleted record is still in the database + asList((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "search", + asList(asList(asList("id", "=", 78))) + ))); + + .. code-tab:: go + + var result bool + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "unlink", + []any{ + []int64{id}, + }, + }, &result); err != nil { + log.Fatal(err) + } + // check if the deleted record is still in the database + var record []any + if err := models.Call("execute_kw", []any{ + db, uid, password, + "res.partner", "search", + []any{[]any{ + []any{"id", "=", id}, + }}, + }, &record); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [] + +Inspection and introspection +---------------------------- + +While we previously used :meth:`~odoo.models.Model.fields_get` to query a +model and have been using an arbitrary model from the start, Odoo stores +most model metadata inside a few meta-models which allow both querying the +system and altering models and fields (with some limitations) on the fly over +XML-RPC. + +.. _reference/webservice/inspection/models: + +``ir.model`` +~~~~~~~~~~~~ + +Provides information about Odoo models via its various fields. + +``name`` + a human-readable description of the model +``model`` + the name of each model in the system +``state`` + whether the model was generated in Python code (``base``) or by creating + an ``ir.model`` record (``manual``) +``field_id`` + list of the model's fields through a :class:`~odoo.fields.One2many` to + :ref:`reference/webservice/inspection/fields` +``view_ids`` + :class:`~odoo.fields.One2many` to the :doc:`../reference/user_interface/view_architectures` + defined for the model +``access_ids`` + :class:`~odoo.fields.One2many` relation to the + :ref:`reference/security/acl` set on the model + +``ir.model`` can be used to + +- Query the system for installed models (as a precondition to operations + on the model or to explore the system's content). +- Get information about a specific model (generally by listing the fields + associated with it). +- Create new models dynamically over RPC. + +.. important:: + * Custom model names must start with ``x_``. + * The ``state`` must be provided and set to ``manual``, otherwise the model will + not be loaded. + * It is not possible to add new *methods* to a custom model, only fields. + +.. example:: + + A custom model will initially contain only the "built-in" fields available + on all models: + + .. tabs:: + + .. code-tab:: python + + models.execute_kw(db, uid, password, 'ir.model', 'create', [{ + 'name': "Custom Model", + 'model': "x_custom_model", + 'state': 'manual', + }]) + models.execute_kw(db, uid, password, 'x_custom_model', 'fields_get', [], {'attributes': ['string', 'help', 'type']}) + + .. code-tab:: php + + $models->execute_kw($db, $uid, $password, 'ir.model', 'create', array(array( + 'name' => "Custom Model", + 'model' => 'x_custom_model', + 'state' => 'manual' + ))); + $models->execute_kw($db, $uid, $password, 'x_custom_model', 'fields_get', array(), array('attributes' => array('string', 'help', 'type'))); + + .. code-tab:: ruby + + models.execute_kw(db, uid, password, 'ir.model', 'create', [{ + name: "Custom Model", + model: 'x_custom_model', + state: 'manual' + }]) + fields = models.execute_kw(db, uid, password, 'x_custom_model', 'fields_get', [], {attributes: %w(string help type)}) + + .. code-tab:: java + + models.execute( + "execute_kw", asList( + db, uid, password, + "ir.model", "create", + asList(new HashMap() {{ + put("name", "Custom Model"); + put("model", "x_custom_model"); + put("state", "manual"); + }}) + )); + final Object fields = models.execute( + "execute_kw", asList( + db, uid, password, + "x_custom_model", "fields_get", + emptyList(), + new HashMap () {{ + put("attributes", asList( + "string", + "help", + "type")); + }} + )); + + .. code-tab:: go + + var id int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "ir.model", "create", + []map[string]string{ + { + "name": "Custom Model", + "model": "x_custom_model", + "state": "manual", + }, + }, + }, &id); err != nil { + log.Fatal(err) + } + recordFields := map[string]string{} + if err := models.Call("execute_kw", []any{ + db, uid, password, + "x_custom_model", "fields_get", + []any{}, + map[string][]string{ + "attributes": {"string", "help", "type"}, + }, + }, &recordFields); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + { + "create_uid": { + "type": "many2one", + "string": "Created by" + }, + "create_date": { + "type": "datetime", + "string": "Created on" + }, + "__last_update": { + "type": "datetime", + "string": "Last Modified on" + }, + "write_uid": { + "type": "many2one", + "string": "Last Updated by" + }, + "write_date": { + "type": "datetime", + "string": "Last Updated on" + }, + "display_name": { + "type": "char", + "string": "Display Name" + }, + "id": { + "type": "integer", + "string": "Id" + } + } + +.. _reference/webservice/inspection/fields: + +``ir.model.fields`` +~~~~~~~~~~~~~~~~~~~ + +Provides information about the fields of Odoo models and allows adding +custom fields without using Python code. + +``model_id`` + :class:`~odoo.fields.Many2one` to + :ref:`reference/webservice/inspection/models` to which the field belongs +``name`` + the field's technical name (used in ``read`` or ``write``) +``field_description`` + the field's user-readable label (e.g. ``string`` in ``fields_get``) +``ttype`` + the :ref:`type ` of field to create +``state`` + whether the field was created via Python code (``base``) or via + ``ir.model.fields`` (``manual``) +``required``, ``readonly``, ``translate`` + enables the corresponding flag on the field +``groups`` + :ref:`field-level access control `, a + :class:`~odoo.fields.Many2many` to ``res.groups`` +``selection``, ``size``, ``on_delete``, ``relation``, ``relation_field``, ``domain`` + type-specific properties and customizations, see :ref:`the fields + documentation ` for details + +.. important:: + - Like custom models, only new fields created with ``state="manual"`` are activated as actual + fields on the model. + - Computed fields can not be added via ``ir.model.fields``, some field meta-information + (defaults, onchange) can not be set either. + +.. example:: + + .. tabs:: + + .. code-tab:: python + + id = models.execute_kw(db, uid, password, 'ir.model', 'create', [{ + 'name': "Custom Model", + 'model': "x_custom", + 'state': 'manual', + }]) + models.execute_kw(db, uid, password, 'ir.model.fields', 'create', [{ + 'model_id': id, + 'name': 'x_name', + 'ttype': 'char', + 'state': 'manual', + 'required': True, + }]) + record_id = models.execute_kw(db, uid, password, 'x_custom', 'create', [{'x_name': "test record"}]) + models.execute_kw(db, uid, password, 'x_custom', 'read', [[record_id]]) + + .. code-tab:: php + + $id = $models->execute_kw($db, $uid, $password, 'ir.model', 'create', array(array( + 'name' => "Custom Model", + 'model' => 'x_custom', + 'state' => 'manual' + ))); + $models->execute_kw($db, $uid, $password, 'ir.model.fields', 'create', array(array( + 'model_id' => $id, + 'name' => 'x_name', + 'ttype' => 'char', + 'state' => 'manual', + 'required' => true + ))); + $record_id = $models->execute_kw($db, $uid, $password, 'x_custom', 'create', array(array('x_name' => "test record"))); + $models->execute_kw($db, $uid, $password, 'x_custom', 'read', array(array($record_id))); + + .. code-tab:: ruby + + id = models.execute_kw(db, uid, password, 'ir.model', 'create', [{ + name: "Custom Model", + model: "x_custom", + state: 'manual' + }]) + models.execute_kw(db, uid, password, 'ir.model.fields', 'create', [{ + model_id: id, + name: "x_name", + ttype: "char", + state: "manual", + required: true + }]) + record_id = models.execute_kw(db, uid, password, 'x_custom', 'create', [{x_name: "test record"}]) + models.execute_kw(db, uid, password, 'x_custom', 'read', [[record_id]]) + + .. code-tab:: java + + final Integer id = (Integer)models.execute( + "execute_kw", asList( + db, uid, password, + "ir.model", "create", + asList(new HashMap() {{ + put("name", "Custom Model"); + put("model", "x_custom"); + put("state", "manual"); + }}) + )); + models.execute( + "execute_kw", asList( + db, uid, password, + "ir.model.fields", "create", + asList(new HashMap() {{ + put("model_id", id); + put("name", "x_name"); + put("ttype", "char"); + put("state", "manual"); + put("required", true); + }}) + )); + final Integer record_id = (Integer)models.execute( + "execute_kw", asList( + db, uid, password, + "x_custom", "create", + asList(new HashMap() {{ + put("x_name", "test record"); + }}) + )); + + client.execute( + "execute_kw", asList( + db, uid, password, + "x_custom", "read", + asList(asList(record_id)) + )); + + .. code-tab:: go + + var id int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "ir.model", "create", + []map[string]string{ + { + "name": "Custom Model", + "model": "x_custom", + "state": "manual", + }, + }, + }, &id); err != nil { + log.Fatal(err) + } + var fieldId int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "ir.model.fields", "create", + []map[string]any{ + { + "model_id": id, + "name": "x_name", + "ttype": "char", + "state": "manual", + "required": true, + }, + }, + }, &fieldId); err != nil { + log.Fatal(err) + } + var recordId int64 + if err := models.Call("execute_kw", []any{ + db, uid, password, + "x_custom", "create", + []map[string]string{ + {"x_name": "test record"}, + }, + }, &recordId); err != nil { + log.Fatal(err) + } + var recordFields []map[string]any + if err := models.Call("execute_kw", []any{ + db, uid, password, + "x_custom", "read", + [][]int64{{recordId}}, + }, recordFields); err != nil { + log.Fatal(err) + } + + Result: + + .. code-block:: json + + [ + { + "create_uid": [1, "Administrator"], + "x_name": "test record", + "__last_update": "2014-11-12 16:32:13", + "write_uid": [1, "Administrator"], + "write_date": "2014-11-12 16:32:13", + "create_date": "2014-11-12 16:32:13", + "id": 1, + "display_name": "test record" + } + ] + +.. _PostgreSQL: https://www.postgresql.org +.. _XML-RPC: https://en.wikipedia.org/wiki/XML-RPC +.. _base64: https://en.wikipedia.org/wiki/Base64