7 Odoo is mostly extended internally via modules, but much of its features and
8 all of its data is also available from the outside for external analysis or
9 integration with various tools. Part of the :ref:`reference/orm/model` API is
10 easily available over XML-RPC_ and accessible from a variety of languages.
12 .. Odoo XML-RPC idiosyncracies:
13 * uses multiple endpoint and a nested call syntax instead of a
14 "hierarchical" server structure (e.g. ``openerp.res.partner.read()``)
15 * uses its own own manual auth system instead of basic auth or sessions
16 (basic is directly supported the Python and Ruby stdlibs as well as
17 ws-xmlrpc, not sure about ripcord)
18 * own auth is inconvenient as (uid, password) have to be explicitly passed
19 into every call. Session would allow db to be stored as well
20 These issues are especially visible in Java, somewhat less so in PHP
22 Connection and authentication
23 =============================
25 .. kinda gross because it duplicates existing bits
29 .. rst-class:: setupcode hidden
31 .. code-block:: python
34 info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
35 url, db, username, password = \
36 info['host'], info['database'], info['user'], info['password']
37 common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
38 uid = common.authenticate(db, username, password, {})
39 models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
43 require "xmlrpc/client"
44 info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start')
45 url, db, username, password = \
46 info['host'], info['database'], info['user'], info['password']
47 common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
48 uid = common.call('authenticate', db, username, password, {})
49 models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
53 require_once('ripcord.php');
54 $info = ripcord::client('https://demo.odoo.com/start')->start();
55 list($url, $db, $username, $password) =
56 array($info['host'], $info['database'], $info['user'], $info['password']);
57 $common = ripcord::client("$url/xmlrpc/2/common");
58 $uid = $common->authenticate($db, $username, $password, array());
59 $models = ripcord::client("$url/xmlrpc/2/object");
63 final XmlRpcClient client = new XmlRpcClient();
65 final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl();
66 start_config.setServerURL(new URL("https://demo.odoo.com/start"));
67 final Map<String, String> info = (Map<String, String>)client.execute(
68 start_config, "start", Collections.emptyList());
70 final String url = info.get("host"),
71 db = info.get("database"),
72 username = info.get("user"),
73 password = info.get("password");
75 final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
76 common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url)));
78 int uid = (int)client.execute(
79 common_config, "authenticate", Arrays.asList(
80 db, username, password, Collections.emptyMap()));
82 final XmlRpcClient models = new XmlRpcClient() {{
83 setConfig(new XmlRpcClientConfigImpl() {{
84 setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
91 If you already have an Odoo server installed, you can just use its
94 .. rst-class:: switchable setup
96 .. code-block:: python
98 url = <insert server URL>
99 db = <insert database name>
101 password = <insert password for your admin user (default: admin)>
105 url = <insert server URL>
106 db = <insert database name>
108 password = <insert password for your admin user (default: admin)>
112 $url = <insert server URL>;
113 $db = <insert database name>;
115 $password = <insert password for your admin user (default: admin)>;
119 final String url = <insert server URL>,
120 db = <insert database name>,
122 password = <insert password for your admin user (default: admin)>;
124 To make exploration simpler, you can also ask https://demo.odoo.com for a test
127 .. rst-class:: switchable setup
129 .. code-block:: python
132 info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
133 url, db, username, password = \
134 info['host'], info['database'], info['user'], info['password']
138 require "xmlrpc/client"
139 info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start')
140 url, db, username, password = \
141 info['host'], info['database'], info['user'], info['password']
145 require_once('ripcord.php');
146 $info = ripcord::client('https://demo.odoo.com/start')->start();
147 list($url, $db, $username, $password) =
148 array($info['host'], $info['database'], $info['user'], $info['password']);
152 final XmlRpcClient client = new XmlRpcClient();
154 final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl();
155 start_config.setServerURL(new URL("https://demo.odoo.com/start"));
156 final Map<String, String> info = (Map<String, String>)client.execute(
157 start_config, "start", Collections.emptyList());
159 final String url = info.get("host"),
160 db = info.get("database"),
161 username = info.get("user"),
162 password = info.get("password");
164 .. rst-class:: force-right
169 These examples use the `Ripcord <https://code.google.com/p/ripcord/>`_
170 library, which provides a simple XML-RPC API. Ripcord requires that
171 `XML-RPC support be enabled
172 <http://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
175 Since calls are performed over
176 `HTTPS <http://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
177 the `OpenSSL extension
178 <http://php.net/manual/en/openssl.installation.php>`_ be enabled.
183 These examples use the `Apache XML-RPC library
184 <https://ws.apache.org/xmlrpc/>`_
189 Odoo requires users of the API to be authenticated before being able to query
192 The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require
193 authentication, such as the authentication itself or fetching version
194 information. To verify if the connection information is correct before trying
195 to authenticate, the simplest call is to ask for the server's version. The
196 authentication itself is done through the ``authenticate`` function and
197 returns a user identifier (``uid``) used in authenticated calls instead of
200 .. rst-class:: switchable setup
202 .. code-block:: python
204 common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
209 common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
210 common.call('version')
214 $common = ripcord::client("$url/xmlrpc/2/common");
219 final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
220 common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url)));
221 client.execute(common_config, "version", Collections.emptyList());
226 "server_version": "8.0",
227 "server_version_info": [8, 0, 0, "final", 0],
228 "server_serie": "8.0",
229 "protocol_version": 1,
232 .. rst-class:: switchable setup
234 .. code-block:: python
236 uid = common.authenticate(db, username, password, {})
240 uid = common.call('authenticate', db, username, password, {})
244 $uid = $common->authenticate($db, $username, $password, array());
248 int uid = (int)client.execute(
249 common_config, "authenticate", Arrays.asList(
250 db, username, password, Collections.emptyMap()));
255 The second endpoint is ``xmlrpc/2/object``, is used to call methods of odoo
256 models via the ``execute_kw`` RPC function.
258 Each call to ``execute_kw`` takes the following parameters:
260 * the database to use, a string
261 * the user id (retrieved through ``authenticate``), an integer
262 * the user's password, a string
263 * the model name, a string
264 * the method name, a string
265 * an array/list of parameters passed by position
266 * a mapping/dict of parameters to pass by keyword (optional)
268 .. rst-class:: force-right
270 For instance to see if we can read the ``res.partner`` model we can call
271 ``check_access_rights`` with ``operation`` passed by position and
272 ``raise_exception`` passed by keyword (in order to get a true/false result
273 rather than true/error):
275 .. rst-class:: switchable setup
277 .. code-block:: python
279 models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
280 models.execute_kw(db, uid, password,
281 'res.partner', 'check_access_rights',
282 ['read'], {'raise_exception': False})
286 models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
287 models.execute_kw(db, uid, password,
288 'res.partner', 'check_access_rights',
289 ['read'], {raise_exception: false})
293 $models = ripcord::client("$url/xmlrpc/2/object");
294 $models->execute_kw($db, $uid, $password,
295 'res.partner', 'check_access_rights',
296 array('read'), array('raise_exception' => false));
300 final XmlRpcClient models = new XmlRpcClient() {{
301 setConfig(new XmlRpcClientConfigImpl() {{
302 setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
305 models.execute("execute_kw", Arrays.asList(
307 "res.partner", "check_access_rights",
308 Arrays.asList("read"),
309 new HashMap() {{ put("raise_exception", false); }}
316 .. todo:: this should be runnable and checked
321 Records can be listed and filtered via :meth:`~openerp.models.Model.search`.
323 :meth:`~openerp.models.Model.search` takes a mandatory
324 :ref:`domain <reference/orm/domains>` filter (possibly empty), and returns the
325 database identifiers of all records matching the filter. To list customer
326 companies for instance:
328 .. rst-class:: switchable
330 .. code-block:: python
332 models.execute_kw(db, uid, password,
333 'res.partner', 'search',
334 [[['is_company', '=', True], ['customer', '=', True]]])
338 models.execute_kw(db, uid, password,
339 'res.partner', 'search',
340 [[['is_company', '=', true], ['customer', '=', true]]])
344 $domain = array(array('is_company', '=', true),
345 array('customer', '=', true));
346 $models->execute_kw($db, $uid, $password,
347 'res.partner', 'search', array($domain));
351 final List domain = Arrays.asList(
352 Arrays.asList("is_company", "=", true),
353 Arrays.asList("customer", "=", true));
354 Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
356 "res.partner", "search",
357 Arrays.asList(domain)
362 [7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74]
367 By default a research will return the ids of all records matching the
368 condition, which may be a huge number. ``offset`` and ``limit`` parameters are
369 available to only retrieve a subset of all matched records.
371 .. rst-class:: switchable
373 .. code-block:: python
375 models.execute_kw(db, uid, password,
376 'res.partner', 'search',
377 [[['is_company', '=', True], ['customer', '=', True]]],
378 {'offset': 10, 'limit': 5})
382 models.execute_kw(db, uid, password,
383 'res.partner', 'search',
384 [[['is_company', '=', true], ['customer', '=', true]]],
385 {offset: 10, limit: 5})
389 $models->execute_kw($db, $uid, $password,
390 'res.partner', 'search',
392 array('offset'=>10, 'limit'=>5));
396 Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
398 "res.partner", "search",
399 Arrays.asList(domain),
400 new HashMap() {{ put("offset", 10); put("limit", 5); }}
410 Rather than retrieve a possibly gigantic list of records and count them
411 afterwards, :meth:`~openerp.models.Model.search_count` can be used to retrieve
412 only the number of records matching the query. It takes the same
413 :ref:`domain <reference/orm/domains>` filter as
414 :meth:`~openerp.models.Model.search` and no other parameter.
416 .. rst-class:: switchable
418 .. code-block:: python
420 models.execute_kw(db, uid, password,
421 'res.partner', 'search_count',
422 [[['is_company', '=', True], ['customer', '=', True]]])
426 models.execute_kw(db, uid, password,
427 'res.partner', 'search_count',
428 [[['is_company', '=', true], ['customer', '=', true]]])
432 $models->execute_kw($db, $uid, $password,
433 'res.partner', 'search_count',
438 (Integer)models.execute("execute_kw", Arrays.asList(
440 "res.partner", "search_count",
441 Arrays.asList(domain)
450 calling ``search`` then ``search_count`` (or the other way around) may not
451 yield coherent results if other users are using the server: stored data
452 could have changed between the calls
457 Record data is accessible via the :meth:`~openerp.models.Model.read` method,
458 which takes a list of ids (as returned by
459 :meth:`~openerp.models.Model.search`) and optionally a list of fields to
460 fetch. By default, it will fetch all the fields the current user can read,
461 which tends to be a huge amount.
463 .. rst-class:: switchable
465 .. code-block:: python
467 ids = models.execute_kw(db, uid, password,
468 'res.partner', 'search',
469 [[['is_company', '=', True], ['customer', '=', True]]],
471 [record] = models.execute_kw(db, uid, password,
472 'res.partner', 'read', [ids])
473 # count the number of fields fetched by default
478 ids = models.execute_kw(db, uid, password,
479 'res.partner', 'search',
480 [[['is_company', '=', true], ['customer', '=', true]]],
482 record = models.execute_kw(db, uid, password,
483 'res.partner', 'read', [ids]).first
484 # count the number of fields fetched by default
489 $ids = $models->execute_kw($db, $uid, $password,
490 'res.partner', 'search',
493 $records = $models->execute_kw($db, $uid, $password,
494 'res.partner', 'read', array($ids));
495 // count the number of fields fetched by default
500 final List ids = Arrays.asList((Object[])models.execute(
501 "execute_kw", Arrays.asList(
503 "res.partner", "search",
504 Arrays.asList(domain),
505 new HashMap() {{ put("limit", 1); }})));
506 final Map record = (Map)((Object[])models.execute(
507 "execute_kw", Arrays.asList(
509 "res.partner", "read",
513 // count the number of fields fetched by default
520 Conversedly, picking only three fields deemed interesting.
522 .. rst-class:: switchable
524 .. code-block:: python
526 models.execute_kw(db, uid, password,
527 'res.partner', 'read',
528 [ids], {'fields': ['name', 'country_id', 'comment']})
532 models.execute_kw(db, uid, password,
533 'res.partner', 'read',
534 [ids], {fields: %w(name country_id comment)})
538 $models->execute_kw($db, $uid, $password,
539 'res.partner', 'read',
541 array('fields'=>array('name', 'country_id', 'comment')));
545 Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
547 "res.partner", "read",
550 put("fields", Arrays.asList("name", "country_id", "comment"));
556 [{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}]
558 .. note:: even if the ``id`` field is not requested, it is always returned
560 Listing record fields
561 ---------------------
563 :meth:`~openerp.models.Model.fields_get` can be used to inspect
564 a model's fields and check which ones seem to be of interest.
567 it returns a great amount of meta-information (it is also used by client
568 programs) it should be filtered before printing, the most interesting items
569 for a human user are ``string`` (the field's label), ``help`` (a help text if
570 available) and ``type`` (to know which values to expect, or to send when
573 .. rst-class:: switchable
575 .. code-block:: python
577 fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
578 # filter keys of field attributes for display
580 k: v for k, v in attributes.iteritems()
581 if k in ['string', 'help', 'type']
583 for field, attributes in fields.iteritems()}
587 fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
588 # filter keys of field attributes for display
590 fields[k] = v.keep_if {|kk, vv| %w(string help type).include? kk}
595 $fields_full = $models->execute_kw($db, $uid, $password,
596 'res.partner', 'fields_get', array());
597 // filter keys of field attributes for display
598 $allowed = array_flip(array('string', 'help', 'type'));
600 foreach($fields_full as $field => $attributes) {
601 $fields[$field] = array_intersect_key($attributes, $allowed);
606 final Map<String, Map<String, Object>> fields =
607 (Map<String, Map<String, Object>>)models.execute("execute_kw", Arrays.asList(
609 "res.partner", "fields_get",
610 Collections.emptyList()));
611 // filter keys of field attributes for display
612 final List<String> allowed = Arrays.asList("string", "help", "type");
613 new HashMap<String, Map<String, Object>>() {{
614 for(Entry<String, Map<String, Object>> item: fields.entrySet()) {
615 put(item.getKey(), new HashMap<String, Object>() {{
616 for(Entry<String, Object> it: item.getValue().entrySet()) {
617 if (allowed.contains(it.getKey())) {
618 put(it.getKey(), it.getValue());
633 "property_account_position": {
635 "help": "The fiscal position will determine taxes and accounts used for the partner.",
636 "string": "Fiscal Position"
641 "string": "Signup Token is Valid"
643 "date_localization": {
646 "string": "Geo Localization Date"
651 "string": "Companies that refers to partner"
653 "sale_order_count": {
656 "string": "# of Sales Order"
658 "purchase_order_count": {
661 "string": "# of Purchase Order"
667 Because that is a very common task, Odoo provides a
668 :meth:`~openerp.models.Model.search_read` shortcut which as its name notes is
669 equivalent to a :meth:`~openerp.models.Model.search` followed by a
670 :meth:`~openerp.models.Model.read`, but avoids having to perform two requests
671 and keep ids around. Its arguments are similar to
672 :meth:`~openerp.models.Model.search`'s, but it can also take a list of
673 ``fields`` (like :meth:`~openerp.models.Model.read`, if that list is not
674 provided it'll fetch all fields of matched records):
676 .. rst-class:: switchable
678 .. code-block:: python
680 models.execute_kw(db, uid, password,
681 'res.partner', 'search_read',
682 [[['is_company', '=', True], ['customer', '=', True]]],
683 {'fields': ['name', 'country_id', 'comment'], 'limit': 5})
687 models.execute_kw(db, uid, password,
688 'res.partner', 'search_read',
689 [[['is_company', '=', true], ['customer', '=', true]]],
690 {fields: %w(name country_id comment), limit: 5})
694 $models->execute_kw($db, $uid, $password,
695 'res.partner', 'search_read',
697 array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5));
701 Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
703 "res.partner", "search_read",
704 Arrays.asList(domain),
706 put("fields", Arrays.asList("name", "country_id", "comment"));
716 "country_id": [ 21, "Belgium" ],
722 "country_id": [ 76, "France" ],
728 "country_id": [ 233, "United Kingdom" ],
730 "name": "Bank Wealthy and sons"
734 "country_id": [ 105, "India" ],
736 "name": "Best Designers"
740 "country_id": [ 76, "France" ],
750 .. rst-class:: switchable
752 .. code-block:: python
754 id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
755 'name': "New Partner",
760 id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
766 $id = $models->execute_kw($db, $uid, $password,
767 'res.partner', 'create',
768 array(array('name'=>"New Partner")));
772 final Integer id = (Integer)models.execute("execute_kw", Arrays.asList(
774 "res.partner", "create",
775 Arrays.asList(new HashMap() {{ put("name", "New Partner"); }})
785 .. rst-class:: switchable
787 .. code-block:: python
789 models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
790 'name': "Newer partner"
792 # get record name after having changed it
793 models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
797 models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
798 name: "Newer partner"
800 # get record name after having changed it
801 models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
805 $models->execute_kw($db, $uid, $password, 'res.partner', 'write',
806 array(array($id), array('name'=>"Newer partner")));
807 // get record name after having changed it
808 $models->execute_kw($db, $uid, $password,
809 'res.partner', 'name_get', array(array($id)));
813 models.execute("execute_kw", Arrays.asList(
815 "res.partner", "write",
818 new HashMap() {{ put("name", "Newer Partner"); }}
821 // get record name after having changed it
822 Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
824 "res.partner", "name_get",
825 Arrays.asList(Arrays.asList(id))
830 [[78, "Newer partner"]]
835 .. rst-class:: switchable
837 .. code-block:: python
839 models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
840 # check if the deleted record is still in the database
841 models.execute_kw(db, uid, password,
842 'res.partner', 'search', [[['id', '=', id]]])
846 models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
847 # check if the deleted record is still in the database
848 models.execute_kw(db, uid, password,
849 'res.partner', 'search', [[['id', '=', id]]])
853 $models->execute_kw($db, $uid, $password,
854 'res.partner', 'unlink',
856 // check if the deleted record is still in the database
857 $models->execute_kw($db, $uid, $password,
858 'res.partner', 'search',
859 array(array(array('id', '=', $id))));
863 models.execute("execute_kw", Arrays.asList(
865 "res.partner", "unlink",
866 Arrays.asList(Arrays.asList(id))));
867 // check if the deleted record is still in the database
868 Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
870 "res.partner", "search",
871 Arrays.asList(Arrays.asList(Arrays.asList("id", "=", 78)))
878 .. _PostgreSQL: http://www.postgresql.org
879 .. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC