[ADD] Stripe-style APIDoc: content
authorXavier Morel <xmo@openerp.com>
Mon, 20 Oct 2014 05:47:14 +0000 (07:47 +0200)
committerXavier Morel <xmo@openerp.com>
Mon, 27 Oct 2014 16:23:16 +0000 (17:23 +0100)
doc/_themes/odoodoc/__init__.py
doc/_themes/odoodoc/layout.html
doc/_themes/odoodoc/static/style.css
doc/_themes/odoodoc/static/style.less
doc/modules.rst
doc/modules/api_integration.rst [new file with mode: 0644]
openerp/addons/base/ir/ir_model.py
openerp/models.py
openerp/service/common.py

index f646839..38ee122 100644 (file)
@@ -25,3 +25,8 @@ class Exercise(admonitions.BaseAdmonition):
 
 from sphinx.locale import admonitionlabels, l_
 admonitionlabels['exercise'] = l_('Exercise')
+
+# monkeypatch PHP lexer to not require <?php
+from sphinx.highlighting import lexers
+from pygments.lexers.web import PhpLexer
+lexers['php'] = PhpLexer(startinline=True)
index 1fcb690..5e469cf 100644 (file)
@@ -21,7 +21,7 @@
 {%- endblock -%}
 
 {%- block content -%}
-  <div class="document-super">
+  <div class="document-super {% if meta is defined %}{{ meta.classes }}{% endif %}">
     {{ super() }}
   </div>
 {%- endblock -%}
index 9b5468f..3ea44b5 100644 (file)
@@ -6174,7 +6174,11 @@ button.close {
     display: none !important;
   }
 }
+* {
+  box-sizing: border-box;
+}
 body {
+  overflow: auto;
   position: relative;
 }
 .document-super {
@@ -6643,20 +6647,11 @@ div.section > h2 {
  *
  * Generated via Pygments
  */
-.highlight {
-  padding: 9px 14px;
-  margin-bottom: 14px;
-  background-color: #f7f7f9 !important;
-  border: 1px solid #e1e1e8;
-  border-radius: 4px;
-}
 .highlight pre {
-  color: #333;
-  padding: 0 45px 0 0;
-  margin-top: 0;
-  margin-bottom: 0;
-  background-color: transparent;
-  border: 0;
+  padding: 4px;
+  font-size: 75%;
+  word-break: normal;
+  word-wrap: normal;
 }
 /*
  * ZeroClipboard styles
@@ -6665,6 +6660,11 @@ div.section > h2 {
   position: relative;
   display: none;
 }
+@media (min-width: 768px) {
+  .zero-clipboard {
+    display: block;
+  }
+}
 .btn-clipboard {
   position: absolute;
   top: 0;
@@ -6684,11 +6684,6 @@ div.section > h2 {
   background-color: #a24689;
   border-color: #a24689;
 }
-@media (min-width: 768px) {
-  .zero-clipboard {
-    display: block;
-  }
-}
 img.align-center {
   display: block;
   margin: 0 auto;
@@ -6710,10 +6705,63 @@ td.field-body > ul {
   margin: 0;
   padding: 0;
 }
-pre {
-  word-break: normal;
-  word-wrap: normal;
-}
 .descclassname {
   opacity: 0.5;
 }
+.stripe .section {
+  margin-bottom: 2em;
+}
+@media (min-width: 992px) {
+  .stripe .section > *,
+  .stripe .section > .force-left {
+    width: 49%;
+    float: left;
+    clear: left;
+  }
+  .stripe .section > .force-right,
+  .stripe .section > [class*=highlight] {
+    float: none;
+    clear: none;
+    margin-left: 51%;
+  }
+  .stripe .section > h1,
+  .stripe .section > h2,
+  .stripe .section > h3,
+  .stripe .section > h4,
+  .stripe .section > h5,
+  .stripe .section > h6 {
+    background-color: rgba(255, 255, 255, 0.7);
+  }
+  .stripe .section > h1,
+  .stripe .section > h2,
+  .stripe .section > h3,
+  .stripe .section > h4,
+  .stripe .section > h5,
+  .stripe .section > h6,
+  .stripe .section > .section {
+    position: relative;
+    width: auto;
+    float: none;
+    clear: both;
+  }
+  .stripe .bodywrapper {
+    position: relative;
+  }
+  .stripe .bodywrapper:before {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 50%;
+    content: "";
+    width: 0;
+    border-left: 1px solid #777777;
+  }
+}
+.stripe .switchable > .highlight,
+.stripe [class*=only-] {
+  display: none;
+}
+.stripe .only-python,
+.stripe .highlight-python > .highlight {
+  display: block;
+}
index ca8be90..182b81d 100644 (file)
 // indent level for various items list e.g. dl, fields lists, ...
 @item-indent: 30px;
 
+* {
+  box-sizing: border-box;
+}
 body {
+  overflow: auto;
   position: relative;
 }
 
@@ -461,21 +465,13 @@ div.section > h2 {
  *
  * Generated via Pygments
  */
-
-.highlight {
-  padding: 9px 14px;
-  margin-bottom: 14px;
-  background-color: #f7f7f9 !important;
-  border: 1px solid #e1e1e8;
-  border-radius: 4px;
-}
 .highlight pre {
-  color: #333;
-  padding: 0 45px 0 0;
-  margin-top: 0;
-  margin-bottom: 0;
-  background-color: transparent;
-  border: 0;
+  padding: 4px;
+
+  font-size: 75%;
+  // code block lines should not wrap
+  word-break: normal;
+  word-wrap: normal;
 }
 
 /*
@@ -485,6 +481,9 @@ div.section > h2 {
 .zero-clipboard {
   position: relative;
   display: none;
+  @media (min-width: @screen-sm-min) {
+    display: block;
+  }
 }
 .btn-clipboard {
   position: absolute;
@@ -506,12 +505,6 @@ div.section > h2 {
   border-color: @brand-primary;
 }
 
-@media (min-width: 768px) {
-  .zero-clipboard {
-    display: block;
-  }
-}
-
 // rST styles
 img.align-center {
   display: block;
@@ -546,13 +539,67 @@ td.field-body {
     padding: 0;
 }
 
-// code block lines should not wrap
-pre {
-  word-break: normal;
-  word-wrap: normal;
-}
-
 // lighten js namespace/class name
 .descclassname {
   opacity: 0.5;
 }
+
+// STRIPE-STYLE PAGES
+.stripe {
+  .section {
+    margin-bottom: 2em;
+  }
+
+  // === columning only on medium+ ===
+  @media (min-width: @screen-md-min) {
+    // column 1
+    .section > *,
+    .section > .force-left {
+      width: 49%;
+      float: left;
+      clear: left;
+    }
+    // column 2
+    .section > .force-right,
+    .section > [class*=highlight] {
+      float: none;
+      clear: none;
+      margin-left: 51%;
+    }
+    // fullwidth elements
+    .section > h1, .section > h2, .section > h3, .section > h4, .section > h5,
+    .section > h6 {
+      background-color: fadeout(@body-bg, 30%);
+    }
+    .section > h1, .section > h2, .section > h3, .section > h4, .section > h5,
+    .section > h6, .section > .section {
+      position: relative;
+      width: auto;
+      float: none;
+      clear: both;
+    }
+
+    .bodywrapper {
+      position: relative;
+      // middle separator
+      &:before {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 50%;
+        content: "";
+        width: 0;
+        border-left: 1px solid @gray-light;
+      }
+    }
+  }
+  // === show/hide code snippets ===
+  .switchable > .highlight,
+  [class*=only-] {
+    display: none;
+  }
+  // must be final rule of page
+  .only-python, .highlight-python > .highlight {
+    display: block;
+  }
+}
index 16133d1..cb5ed8d 100644 (file)
@@ -5,3 +5,4 @@ Module Objects
 .. toctree::
     :titlesonly:
 
+    modules/api_integration
diff --git a/doc/modules/api_integration.rst b/doc/modules/api_integration.rst
new file mode 100644 (file)
index 0000000..f6a51f0
--- /dev/null
@@ -0,0 +1,816 @@
+:classes: stripe
+
+===========
+Odoo as API
+===========
+
+Odoo is mostly extended internally via modules, but much of its features and
+all of its data is 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 XML-RPC idiosyncracies:
+   * uses multiple endpoint and a nested call syntax instead of a
+     "hierarchical" server structure (e.g. ``openerp.res.partner.read()``)
+   * uses its own own manual auth system instead of basic auth or sessions
+     (basic is directly supported the Python and Ruby stdlibs as well as
+     ws-xmlrpc, not sure about ripcord)
+   * own auth is inconvenient as (uid, password) have to be explicitly passed
+     into every call. Session would allow db to be stored as well
+   These issues are especially visible in Java, somewhat less so in PHP
+
+Connection and authentication
+=============================
+
+Configuration
+-------------
+
+If you already have an Odoo server installed, you can just use its
+parameters
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        url = <insert server URL>
+        db = <insert database name>
+        username = 'admin'
+        password = <insert password for your admin user (default: admin)>
+
+    .. code-block:: ruby
+
+        url = <insert server URL>
+        db = <insert database name>
+        username = "admin"
+        password = <insert password for your admin user (default: admin)>
+
+    .. code-block:: php
+
+        $url = <insert server URL>;
+        $db = <insert database name>;
+        $username = "admin";
+        $password = <insert password for your admin user (default: admin)>;
+
+    .. code-block:: java
+
+        final String url = <insert server URL>,
+                      db = <insert database name>,
+                username = "admin",
+                password = <insert password for your admin user (default: admin)>;
+
+To make exploration simpler, you can also ask https://demo.odoo.com for a test
+database:
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        import xmlrpclib
+        info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
+        url, db, username, password = \
+            info['host'], info['database'], info['user'], info['password']
+
+    .. code-block:: 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']
+
+    .. 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']);
+
+    .. 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<String, String> info = (Map<String, String>)client.execute(
+            start_config, "start", Collections.emptyList());
+
+        final String url = info.get("host"),
+                      db = info.get("database"),
+                username = info.get("user"),
+                password = info.get("password");
+
+.. rst-class:: force-right
+
+    .. note::
+        :class: only-php
+
+        These examples use the `Ripcord <https://code.google.com/p/ripcord/>`_
+        library, which provides a simple XML-RPC API. Ripcord requires that
+        `XML-RPC support be enabled
+        <http://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
+        installation.
+
+        Since calls are performed over
+        `HTTPS <http://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
+        the `OpenSSL extension
+        <http://php.net/manual/en/openssl.installation.php>`_ be enabled.
+
+    .. note::
+        :class: only-java
+
+        These examples use the `Apache XML-RPC library
+        <https://ws.apache.org/xmlrpc/>`_
+
+Logging in
+----------
+
+Odoo requires users of the API to be authenticated before being able to query
+much 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.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
+        common.version()
+
+    .. code-block:: ruby
+
+        common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
+        common.call('version')
+
+    .. code-block:: php
+
+        $common = ripcord::client("$url/xmlrpc/2/common");
+        $common->version();
+
+    .. code-block:: java
+
+        final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
+        common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url)));
+        client.execute(common_config, "version", Collections.emptyList());
+
+.. code-block:: json
+
+    {
+        "server_version": "8.0",
+        "server_version_info": [8, 0, 0, "final", 0],
+        "server_serie": "8.0",
+        "protocol_version": 1,
+    }
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        uid = common.authenticate(db, username, password, {})
+
+    .. code-block:: ruby
+
+        uid = common.call('authenticate', db, username, password, {})
+
+    .. code-block:: php
+
+        $uid = $common->authenticate($db, $username, $password, array());
+
+    .. code-block:: java
+
+        int uid = (int)client.execute(
+            common_config, "authenticate", Arrays.asList(
+                db, username, password, Collections.emptyMap()));
+
+Calling methods
+===============
+
+The second — and most generally useful — is ``xmlrpc/2/object`` which 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)
+
+.. rst-class:: force-right
+
+For instance to see if we can read the ``res.partner`` model we can call
+``check_access_rights`` with ``operation`` passed by position and
+``raise_exception`` passed by keyword (in order to get a true/false result
+rather than true/error):
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
+        models.execute_kw(db, uid, password,
+            'res.partner', 'check_access_rights',
+            ['read'], {'raise_exception': False})
+
+    .. code-block:: ruby
+
+        models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
+        models.execute_kw(db, uid, password,
+            'res.partner', 'check_access_rights',
+            ['read'], {raise_exception: false})
+
+    .. code-block:: php
+
+        $models = ripcord::client("$url/xmlrpc/2/object");
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'check_access_rights',
+            array('read'), array('raise_exception' => false));
+
+    .. code-block:: java
+
+        final XmlRpcClient models = new XmlRpcClient() {{
+            setConfig(new XmlRpcClientConfigImpl() {{
+                setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
+            }});
+        }};
+        models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "check_access_rights",
+            Arrays.asList("read"),
+            new HashMap() {{ put("raise_exception", false); }}
+        ));
+
+.. code-block:: json
+
+    true
+
+.. todo:: this should be runnable and checked
+
+List records
+------------
+
+Records can be listed and filtered via :meth:`~openerp.models.Model.search`.
+
+:meth:`~openerp.models.Model.search` takes a mandatory
+:ref:`domain <reference/orm/domains>` filter (possibly empty), and returns the
+database identifiers of all records matching the filter. To list customer
+companies for instance:
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', True], ['customer', '=', True]]])
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', true], ['customer', '=', true]]])
+
+    .. code-block:: php
+
+        $domain = array(array('is_company', '=', true),
+                        array('customer', '=', true));
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search', array($domain));
+
+    .. code-block:: java
+
+        final List domain = Arrays.asList(
+            Arrays.asList("is_company", "=", true),
+            Arrays.asList("customer", "=", true));
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search",
+            Arrays.asList(domain)
+        )));
+
+.. 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 research 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.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', True], ['customer', '=', True]]],
+            {'offset': 10, 'limit': 5})
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', true], ['customer', '=', true]]],
+            {offset: 10, limit: 5})
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search',
+            array($domain),
+            array('offset'=>10, 'limit'=>5));
+
+    .. code-block:: java
+
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search",
+            Arrays.asList(domain),
+            new HashMap() {{ put("offset", 10); put("limit", 5); }}
+        )));
+
+.. code-block:: json
+
+    [13, 20, 30, 22, 29]
+
+Count records
+-------------
+
+Rather than retrieve a possibly gigantic list of records and count them
+afterwards, :meth:`~openerp.models.Model.search_count` can be used to retrieve
+only the number of records matching the query. It takes the same
+:ref:`domain <reference/orm/domains>` filter as
+:meth:`~openerp.models.Model.search` and no other parameter.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_count',
+            [[['is_company', '=', True], ['customer', '=', True]]])
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_count',
+            [[['is_company', '=', true], ['customer', '=', true]]])
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search_count',
+            array($domain));
+
+    .. code-block:: java
+
+        (Integer)models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search_count",
+            Arrays.asList(domain)
+        ));
+
+.. code-block:: json
+
+    19
+
+.. warning::
+
+    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 is accessible via the :meth:`~openerp.models.Model.read` method,
+which takes a list of ids (as returned by
+:meth:`~openerp.models.Model.search`) and optionally a list of fields to
+fetch. By default, it will fetch all the fields the current user can read,
+which tends to be a huge amount.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        ids = models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', True], ['customer', '=', 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-block:: ruby
+
+        ids = models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', true], ['customer', '=', 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-block:: php
+
+        $ids = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search',
+            array($domain),
+            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-block:: java
+
+        final List ids = Arrays.asList((Object[])models.execute(
+            "execute_kw", Arrays.asList(
+                db, uid, password,
+                "res.partner", "search",
+                Arrays.asList(domain),
+                new HashMap() {{ put("limit", 1); }})));
+        final Map record = (Map)((Object[])models.execute(
+            "execute_kw", Arrays.asList(
+                db, uid, password,
+                "res.partner", "read",
+                Arrays.asList(ids)
+            )
+        ))[0];
+        // count the number of fields fetched by default
+        record.size();
+
+.. code-block:: json
+
+    121
+
+Conversedly, picking only three fields deemed interesting.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'read',
+            [ids], {'fields': ['name', 'country_id', 'comment']})
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'read',
+            [ids], {fields: %w(name country_id comment)})
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'read',
+            array($ids),
+            array('fields'=>array('name', 'country_id', 'comment')));
+
+    .. code-block:: java
+
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "read",
+            Arrays.asList(ids),
+            new HashMap() {{
+                put("fields", Arrays.asList("name", "country_id", "comment"));
+            }}
+        )));
+
+.. 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
+
+Listing record fields
+---------------------
+
+:meth:`~openerp.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 great 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):
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
+        # filter keys of field attributes for display
+        {field: {
+                    k: v for k, v in attributes.iteritems()
+                    if k in ['string', 'help', 'type']
+                }
+         for field, attributes in fields.iteritems()}
+
+    .. code-block:: ruby
+
+        fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
+        # filter keys of field attributes for display
+        fields.each {|k, v|
+            fields[k] = v.keep_if {|kk, vv| %w(string help type).include? kk}
+        }
+
+    .. code-block:: php
+
+        $fields_full = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'fields_get', array());
+        // filter keys of field attributes for display
+        $allowed = array_flip(array('string', 'help', 'type'));
+        $fields = array();
+        foreach($fields_full as $field => $attributes) {
+          $fields[$field] = array_intersect_key($attributes, $allowed);
+        }
+
+    .. code-block:: java
+
+        final Map<String, Map<String, Object>> fields  =
+            (Map<String, Map<String, Object>>)models.execute("execute_kw", Arrays.asList(
+                db, uid, password,
+                "res.partner", "fields_get",
+                Collections.emptyList()));
+        // filter keys of field attributes for display
+        final List<String> allowed = Arrays.asList("string", "help", "type");
+        new HashMap<String, Map<String, Object>>() {{
+            for(Entry<String, Map<String, Object>> item: fields.entrySet()) {
+                put(item.getKey(), new HashMap<String, Object>() {{
+                    for(Entry<String, Object> it: item.getValue().entrySet()) {
+                        if (allowed.contains(it.getKey())) {
+                            put(it.getKey(), it.getValue());
+                        }
+                    }
+                }});
+            }
+        }};
+
+.. code-block:: json
+
+    {
+        "ean13": {
+            "type": "char",
+            "help": "BarCode",
+            "string": "EAN13"
+        },
+        "property_account_position": {
+            "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_companies": {
+            "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 that is a very common task, Odoo provides a
+:meth:`~openerp.models.Model.search_read` shortcut which as its name notes is
+equivalent to a :meth:`~openerp.models.Model.search` followed by a
+:meth:`~openerp.models.Model.read`, but avoids having to perform two requests
+and keep ids around. Its arguments are similar to
+:meth:`~openerp.models.Model.search`'s, but it can also take a list of
+``fields`` (like :meth:`~openerp.models.Model.read`, if that list is not
+provided it'll fetch all fields of matched records):
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_read',
+            [[['is_company', '=', True], ['customer', '=', True]]],
+            {'fields': ['name', 'country_id', 'comment'], 'limit': 5})
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_read',
+            [[['is_company', '=', true], ['customer', '=', true]]],
+            {fields: %w(name country_id comment), limit: 5})
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search_read',
+            array($domain),
+            array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5));
+
+    .. code-block:: java
+
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search_read",
+            Arrays.asList(domain),
+            new HashMap() {{
+                put("fields", Arrays.asList("name", "country_id", "comment"));
+                put("limit", 5);
+            }}
+        )));
+
+.. 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
+--------------
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
+            'name': "New Partner",
+        }])
+
+    .. code-block:: ruby
+
+        id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
+            name: "New Partner",
+        }])
+
+    .. code-block:: php
+
+        $id = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'create',
+            array(array('name'=>"New Partner")));
+
+    .. code-block:: java
+
+        final Integer id = (Integer)models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "create",
+            Arrays.asList(new HashMap() {{ put("name", "New Partner"); }})
+        ));
+
+.. code-block:: json
+
+    78
+
+Update records
+--------------
+
+.. rst-class:: switchable
+
+    .. code-block:: 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', 'name_get', [[id]])
+
+    .. code-block:: 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', 'name_get', [[id]])
+
+    .. code-block:: 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', 'name_get', array(array($id)));
+
+    .. code-block:: java
+
+        models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "write",
+            Arrays.asList(
+                Arrays.asList(id),
+                new HashMap() {{ put("name", "Newer Partner"); }}
+            )
+        ));
+        // get record name after having changed it
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "name_get",
+            Arrays.asList(Arrays.asList(id))
+        )));
+
+.. code-block:: json
+
+    [[78, "Newer partner"]]
+
+Delete records
+--------------
+
+.. rst-class:: switchable
+
+    .. code-block:: 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-block:: 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-block:: 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-block:: java
+
+        models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "unlink",
+            Arrays.asList(Arrays.asList(id))));
+        // check if the deleted record is still in the database
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search",
+            Arrays.asList(Arrays.asList(Arrays.asList("id", "=", 78)))
+        )));
+
+.. code-block:: json
+
+    []
+
+.. _PostgreSQL: http://www.postgresql.org
+.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
index 69099cf..f41d5e3 100644 (file)
@@ -781,7 +781,7 @@ class ir_model_access(osv.osv):
             _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
             msg = '%s %s' % (msg_heads[mode], msg_tail)
             raise openerp.exceptions.AccessError(msg % msg_params)
-        return r or False
+        return bool(r)
 
     __cache_clearing_methods = []
 
index 7c87d15..143ac53 100644 (file)
@@ -1654,7 +1654,7 @@ class BaseModel(object):
 
     @api.returns('self')
     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
-        """ search(args[, offset=0][, limit=None][, order=None][, count=False])
+        """ search(args[, offset=0][, limit=None][, order=None])
 
         Searches for records based on the ``args``
         :ref:`search domain <reference/orm/domains>`.
@@ -1664,9 +1664,6 @@ class BaseModel(object):
         :param int offset: number of results to ignore (default: none)
         :param int limit: maximum number of records to return (default: all)
         :param str order: sort string
-        :param bool count: if ``True``, the call should return the number of
-                           records matching ``args`` rather than the records
-                           themselves.
         :returns: at most ``limit`` records matching the search criteria
 
         :raise AccessError: * if user tries to bypass access rules for read on the requested object.
index 97f36b7..16cbb5d 100644 (file)
@@ -18,14 +18,8 @@ RPC_VERSION_1 = {
 }
 
 def dispatch(method, params):
-    if method in ['login', 'about', 'timezone_get',
-                  'version', 'authenticate']:
-        pass
-    elif method in ['set_loglevel']:
-        passwd = params[0]
-        params = params[1:]
-        security.check_super(passwd)
-    else:
+    if method not in ['login', 'about', 'timezone_get',
+                      'version', 'authenticate', 'set_loglevel']:
         raise Exception("Method not found: %s" % method)
 
     fn = globals()['exp_' + method]