[ADD] ws doc: introspection, reports and workflows
authorXavier Morel <xmo@openerp.com>
Mon, 24 Nov 2014 07:52:38 +0000 (08:52 +0100)
committerXavier Morel <xmo@openerp.com>
Mon, 24 Nov 2014 07:54:55 +0000 (08:54 +0100)
* use static imports in java examples to make them terser
* inline ``domain`` in java and php example to make examples more
  self-contained
* try to extend/improve Model.write's docstring
* add convenience kwarg to fields_get, mostly for user-driven
  introspection

Closes #3689

doc/api_integration.rst
doc/reference/security.rst
doc/reference/workflows.rst
openerp/fields.py
openerp/models.py

index 4a9835f..fef6dfa 100644 (file)
@@ -61,11 +61,10 @@ Connection and authentication
         .. 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());
+                start_config, "start", emptyList());
 
             final String url = info.get("host"),
                           db = info.get("database"),
@@ -77,7 +76,7 @@ Connection and authentication
 
             int uid = (int)client.execute(
                 common_config, "authenticate", Arrays.asList(
-                    db, username, password, Collections.emptyMap()));
+                    db, username, password, emptyMap()));
 
             final XmlRpcClient models = new XmlRpcClient() {{
                 setConfig(new XmlRpcClientConfigImpl() {{
@@ -154,7 +153,7 @@ database:
         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());
+            start_config, "start", emptyList());
 
         final String url = info.get("host"),
                       db = info.get("database"),
@@ -183,6 +182,9 @@ database:
         These examples use the `Apache XML-RPC library
         <https://ws.apache.org/xmlrpc/>`_
 
+        The examples do not include imports as these imports couldn't be
+        pasted in the code.
+
 Logging in
 ----------
 
@@ -217,8 +219,9 @@ the login.
     .. 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());
+        common_config.setServerURL(
+            new URL(String.format("%s/xmlrpc/2/common", url)));
+        client.execute(common_config, "version", emptyList());
 
 .. code-block:: json
 
@@ -246,8 +249,8 @@ the login.
     .. code-block:: java
 
         int uid = (int)client.execute(
-            common_config, "authenticate", Arrays.asList(
-                db, username, password, Collections.emptyMap()));
+            common_config, "authenticate", asList(
+                db, username, password, emptyMap()));
 
 Calling methods
 ===============
@@ -302,10 +305,10 @@ rather than true/error):
                 setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
             }});
         }};
-        models.execute("execute_kw", Arrays.asList(
+        models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "check_access_rights",
-            Arrays.asList("read"),
+            asList("read"),
             new HashMap() {{ put("raise_exception", false); }}
         ));
 
@@ -341,20 +344,19 @@ companies for instance:
 
     .. code-block:: php
 
-        $domain = array(array('is_company', '=', true),
-                        array('customer', '=', true));
         $models->execute_kw($db, $uid, $password,
-            'res.partner', 'search', array($domain));
+            'res.partner', 'search', array(
+                array(array('is_company', '=', true),
+                      array('customer', '=', true))));
 
     .. 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(
+        asList((Object[])models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "search",
-            Arrays.asList(domain)
+            asList(asList(
+                asList("is_company", "=", true),
+                asList("customer", "=", true)))
         )));
 
 .. code-block:: json
@@ -388,15 +390,18 @@ available to only retrieve a subset of all matched records.
 
         $models->execute_kw($db, $uid, $password,
             'res.partner', 'search',
-            array($domain),
+            array(array(array('is_company', '=', true),
+                        array('customer', '=', true))),
             array('offset'=>10, 'limit'=>5));
 
     .. code-block:: java
 
-        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+        asList((Object[])models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "search",
-            Arrays.asList(domain),
+            asList(asList(
+                asList("is_company", "=", true),
+                asList("customer", "=", true))),
             new HashMap() {{ put("offset", 10); put("limit", 5); }}
         )));
 
@@ -431,14 +436,17 @@ only the number of records matching the query. It takes the same
 
         $models->execute_kw($db, $uid, $password,
             'res.partner', 'search_count',
-            array($domain));
+            array(array(array('is_company', '=', true),
+                        array('customer', '=', true))));
 
     .. code-block:: java
 
-        (Integer)models.execute("execute_kw", Arrays.asList(
+        (Integer)models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "search_count",
-            Arrays.asList(domain)
+            asList(asList(
+                asList("is_company", "=", true),
+                asList("customer", "=", true)))
         ));
 
 .. code-block:: json
@@ -488,7 +496,8 @@ which tends to be a huge amount.
 
         $ids = $models->execute_kw($db, $uid, $password,
             'res.partner', 'search',
-            array($domain),
+            array(array(array('is_company', '=', true),
+                        array('customer', '=', true))),
             array('limit'=>1));
         $records = $models->execute_kw($db, $uid, $password,
             'res.partner', 'read', array($ids));
@@ -497,17 +506,19 @@ which tends to be a huge amount.
 
     .. code-block:: java
 
-        final List ids = Arrays.asList((Object[])models.execute(
-            "execute_kw", Arrays.asList(
+        final List ids = asList((Object[])models.execute(
+            "execute_kw", asList(
                 db, uid, password,
                 "res.partner", "search",
-                Arrays.asList(domain),
+                asList(asList(
+                    asList("is_company", "=", true),
+                    asList("customer", "=", true))),
                 new HashMap() {{ put("limit", 1); }})));
         final Map record = (Map)((Object[])models.execute(
-            "execute_kw", Arrays.asList(
+            "execute_kw", asList(
                 db, uid, password,
                 "res.partner", "read",
-                Arrays.asList(ids)
+                asList(ids)
             )
         ))[0];
         // count the number of fields fetched by default
@@ -542,12 +553,12 @@ Conversedly, picking only three fields deemed interesting.
 
     .. code-block:: java
 
-        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+        asList((Object[])models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "read",
-            Arrays.asList(ids),
+            asList(ids),
             new HashMap() {{
-                put("fields", Arrays.asList("name", "country_id", "comment"));
+                put("fields", asList("name", "country_id", "comment"));
             }}
         )));
 
@@ -574,53 +585,32 @@ updating a record):
 
     .. 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()}
+        models.execute_kw(
+            db, uid, password, 'res.partner', 'fields_get',
+            [], {'attributes': ['string', 'help', 'type']})
 
     .. 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}
-        }
+        models.execute_kw(
+            db, uid, password, 'res.partner', 'fields_get',
+            [], {attributes: %w(string help type)})
 
     .. 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);
-        }
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'fields_get',
+            array(), array('attributes' => array('string', 'help', 'type')));
 
     .. 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());
-                        }
-                    }
-                }});
-            }
-        }};
+        (Map<String, Map<String, Object>>)models.execute("execute_kw", asList(
+            db, uid, password,
+            "res.partner", "fields_get",
+            emptyList(),
+            new HashMap() {{
+                put("attributes", asList("string", "help", "type"));
+            }}
+        ));
 
 .. code-block:: json
 
@@ -668,10 +658,11 @@ 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):
+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
 
@@ -693,17 +684,20 @@ provided it'll fetch all fields of matched records):
 
         $models->execute_kw($db, $uid, $password,
             'res.partner', 'search_read',
-            array($domain),
+            array(array(array('is_company', '=', true),
+                        array('customer', '=', true))),
             array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5));
 
     .. code-block:: java
 
-        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+        asList((Object[])models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "search_read",
-            Arrays.asList(domain),
+            asList(asList(
+                asList("is_company", "=", true),
+                asList("customer", "=", true))),
             new HashMap() {{
-                put("fields", Arrays.asList("name", "country_id", "comment"));
+                put("fields", asList("name", "country_id", "comment"));
                 put("limit", 5);
             }}
         )));
@@ -747,6 +741,13 @@ provided it'll fetch all fields of matched records):
 Create records
 --------------
 
+Records of a model are created using :meth:`~openerp.models.Model.create`. The
+method will create a single record and return its database identifier.
+
+:meth:`~openerp.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.
+
 .. rst-class:: switchable
 
     .. code-block:: python
@@ -769,19 +770,40 @@ Create records
 
     .. code-block:: java
 
-        final Integer id = (Integer)models.execute("execute_kw", Arrays.asList(
+        final Integer id = (Integer)models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "create",
-            Arrays.asList(new HashMap() {{ put("name", "New Partner"); }})
+            asList(new HashMap() {{ put("name", "New Partner"); }})
         ));
 
 .. code-block:: json
 
     78
 
+.. warning::
+
+    while most value types are what would be expected (integer for
+    :class:`~openerp.fields.Integer`, string for :class:`~openerp.fields.Char`
+    or :class:`~openerp.fields.Text`),
+
+    * :class:`~openerp.fields.Date`, :class:`~openerp.fields.Datetime` and
+      :class:`~openerp.fields.Binary` fields use string values
+    * :class:`~openerp.fields.One2many` and :class:`~openerp.fields.Many2many`
+      use a special command protocol detailed in :meth:`the documentation to
+      the write method <openerp.models.Model.write>`.
+
 Update records
 --------------
 
+Reccords can be updated using :meth:`~openerp.models.Model.write`, it takes
+a list of records to update and a mapping of updated fields to values similar
+to :meth:`~openerp.models.Model.create`.
+
+Multiple records can be updated simultanously, but they will all get the same
+values for the fields being set. It is not currently possible to perform
+"computed" updates (where the value being set depends on an existing value of
+a record).
+
 .. rst-class:: switchable
 
     .. code-block:: python
@@ -810,19 +832,19 @@ Update records
 
     .. code-block:: java
 
-        models.execute("execute_kw", Arrays.asList(
+        models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "write",
-            Arrays.asList(
-                Arrays.asList(id),
+            asList(
+                asList(id),
                 new HashMap() {{ put("name", "Newer Partner"); }}
             )
         ));
         // get record name after having changed it
-        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+        asList((Object[])models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "name_get",
-            Arrays.asList(Arrays.asList(id))
+            asList(asList(id))
         )));
 
 .. code-block:: json
@@ -832,6 +854,9 @@ Update records
 Delete records
 --------------
 
+Records can be deleted in bulk by providing the ids of all records to remove
+to :meth:`~openerp.models.Model.unlink`.
+
 .. rst-class:: switchable
 
     .. code-block:: python
@@ -860,20 +885,561 @@ Delete records
 
     .. code-block:: java
 
-        models.execute("execute_kw", Arrays.asList(
+        models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "unlink",
-            Arrays.asList(Arrays.asList(id))));
+            asList(asList(id))));
         // check if the deleted record is still in the database
-        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+        asList((Object[])models.execute("execute_kw", asList(
             db, uid, password,
             "res.partner", "search",
-            Arrays.asList(Arrays.asList(Arrays.asList("id", "=", 78)))
+            asList(asList(asList("id", "=", 78)))
         )));
 
 .. code-block:: json
 
     []
 
+Inspection and introspection
+----------------------------
+
+.. todo:: ``get_external_id`` is kinda crap and may not return an id: it just
+          gets a random existing xid but won't generate one if there is no
+          xid currently associated with the record. And operating with xids
+          isn't exactly fun in RPC.
+
+While we previously used :meth:`~openerp.models.Model.fields_get` to query a
+model's 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 informations about Odoo models themselves 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:`~openerp.fields.One2many` to
+    :ref:`reference/webservice/inspection/fields`
+``view_ids``
+    :class:`~openerp.fields.One2many` to the :ref:`reference/views` defined
+    for the model
+``access_ids``
+    :class:`~openerp.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
+
+.. warning::
+
+    * "custom" model names must start with ``x_``
+    * the ``state`` must be provided and ``manual``, otherwise the model will
+      not be loaded
+    * it is not possible to add new *methods* to a custom model, only fields
+
+.. rst-class:: force-right
+
+    a custom model will initially contain only the "built-in" fields available
+    on all models:
+
+.. rst-class:: switchable
+
+    .. code-block:: 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-block:: 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-block:: 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-block:: java
+
+        models.execute(
+            "execute_kw", asList(
+                db, uid, password,
+                "ir.model", "create",
+                asList(new HashMap<String, Object>() {{
+                    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<String, Object> () {{
+                    put("attributes", asList(
+                            "string",
+                            "help",
+                            "type"));
+                }}
+        ));
+
+.. 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 informations about the fields of Odoo models and allows adding
+custom fields without using Python code
+
+``model_id``
+    :class:`~openerp.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 <reference/orm/fields>` 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 <reference/security/fields>`, a
+    :class:`~openerp.fields.Many2many` to ``res.groups``
+``selection``, ``size``, ``on_delete``, ``relation``, ``relation_field``, ``domain``
+    type-specific properties and customizations, see :ref:`the fields
+    documentation <reference/orm/fields>` for details
+
+Like custom models, only new fields created with ``state="manual"`` are
+activated as actual fields on the model.
+
+.. warning:: computed fields can not be added via ``ir.model.fields``, some
+             field meta-information (defaults, onchange) can not be set either
+
+.. todo:: maybe new-API fields could store constant ``default`` in a new
+          column, maybe JSON-encoded?
+
+.. rst-class:: switchable
+
+    .. code-block:: 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-block:: 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-block:: 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-block:: java
+
+        final Integer id = (Integer)models.execute(
+            "execute_kw", asList(
+                db, uid, password,
+                "ir.model", "create",
+                asList(new HashMap<String, Object>() {{
+                    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<String, Object>() {{
+                    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<String, Object>() {{
+                    put("x_name", "test record");
+                }})
+        ));
+
+        client.execute(
+            "execute_kw", asList(
+                db, uid, password,
+                "x_custom", "read",
+                asList(asList(record_id))
+        ));
+
+.. 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"
+        }
+    ]
+
+Workflow manipulations
+----------------------
+
+:ref:`reference/workflows` can be moved along by sending them *signals*.
+Instead of using the top-level ``execute_kw``, signals are sent using
+``exec_workflow``.
+
+Signals are sent to a specific record, and possibly trigger a transition on
+the workflow instance associated with the record.
+
+.. warning:: requires that the ``account`` module be installed
+    :class: force-right
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        client = models.execute_kw(
+            db, uid, password,
+            'res.partner', 'search_read',
+            [[('customer', '=', True)]],
+            {'limit': 1, 'fields': [
+                'property_account_receivable',
+                'property_payment_term',
+                'property_account_position']
+            })[0]
+        invoice_id = models.execute_kw(
+            db, uid, password,
+            'account.invoice', 'create', [{
+                'partner_id': client['id'],
+                'account_id': client['property_account_receivable'][0],
+                'invoice_line': [(0, False, {'name': "AAA"})]
+            }])
+
+        models.exec_workflow(
+            db, uid, password, 'account.invoice', 'invoice_open', invoice_id)
+
+    .. code-block:: php
+
+        $client = $models->execute_kw(
+            $db, $uid, $password,
+            'res.partner', 'search_read',
+            array(array(array('customer', '=', true))),
+            array(
+                'limit' => 1,
+                'fields' => array(
+                    'property_account_receivable',
+                    'property_payment_term',
+                    'property_account_position'
+                )))[0];
+        $invoice_id = $models->execute_kw(
+            $db, $uid, $password,
+            'account.invoice', 'create', array(array(
+                'partner_id' => $client['id'],
+                'account_id' => $client['property_account_receivable'][0],
+                'invoice_line' => array(array(0, false, array('name' => "AAA")))
+            )));
+
+        $models->exec_workflow(
+            $db, $uid, $password,
+            'account.invoice', 'invoice_open',
+             $invoice_id);
+
+    .. code-block:: ruby
+
+        client = models.execute_kw(
+            db, uid, password,
+            'res.partner', 'search_read',
+            [[['customer', '=', true]]],
+            {limit: 1, fields: %w(property_account_receivable property_payment_term property_account_position)}
+        )[0]
+        invoice_id = models.execute_kw(
+            db, uid, password,
+            'account.invoice', 'create', [{
+                partner_id: client['id'],
+                account_id: client['property_account_receivable'][0],
+                invoice_line: [[0, false, {name: "AAA"}]]
+            }])
+
+        models.exec_workflow(
+            db, uid, password,
+            'account.invoice', 'invoice_open', invoice_id)
+
+    .. code-block:: java
+
+        final Map<String, Object> c = (Map<String, Object>)
+            ((Object[])models.execute("execute_kw", asList(
+                    db, uid, password,
+                    "res.partner", "search_read",
+                    asList(
+                        asList(
+                            asList("customer", "=", true))),
+                    new HashMap<String, Object>() {{
+                            put("limit", 1);
+                            put("fields", asList(
+                                "property_account_receivable",
+                                "property_payment_term",
+                                "property_account_position"
+                            ));
+                        }}
+            )))[0];
+        final Integer invoice_id = (Integer)models.execute(
+            "execute_kw", asList(
+                db, uid, password,
+                "account.invoice", "create",
+                asList(new HashMap<String, Object>() {{
+                    put("partner_id", c.get("id"));
+                    put("account_id", ((Object[])c.get("property_account_receivable"))[0]);
+                    put("invoice_line", asList(
+                        asList(0, false, new HashMap<String, Object>() {{
+                            put("name", "AAA");
+                        }})
+                    ));
+                }})
+        ));
+
+        models.execute(
+            "exec_workflow", asList(
+                db, uid, password,
+                "account.invoice", "invoice_open", invoice_id));
+
+Report printing
+---------------
+
+Available reports can be listed by searching the ``ir.actions.report.xml``
+model, fields of interest being
+
+``model``
+    the model on which the report applies, can be used to look for available
+    reports on a specific model
+``name``
+    human-readable report name
+``report_name``
+    the technical name of the report, used to print it
+
+Reports can be printed over RPC with the following information:
+
+* the name of the report (``report_name``)
+* the ids of the records to include in the report
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        invoice_ids = models.execute_kw(
+            db, uid, password, 'account.invoice', 'search',
+            [[('type', '=', 'out_invoice'), ('state', '=', 'open')]])
+        report = xmlrpclib.ServerProxy('{}/xmlrpc/2/report'.format(url))
+        result = report.render_report(
+            db, uid, password, 'account.report_invoice', invoice_ids)
+        report_data = result['result'].decode('base64')
+
+    .. code-block:: php
+
+        $invoice_ids = $models->execute_kw(
+            $db, $uid, $password,
+            'account.invoice', 'search',
+            array(array(array('type', '=', 'out_invoice'),
+                        array('state', '=', 'open'))));
+        $report = ripcord::client("$url/xmlrpc/2/report");
+        $result = $report->render_report(
+            $db, $uid, $password,
+            'account.report_invoice', $invoice_ids);
+        $report_data = base64_decode($result['result']);
+
+    .. code-block:: ruby
+
+        require 'base64'
+        invoice_ids = models.execute_kw(
+            db, uid, password,
+            'account.invoice', 'search',
+            [[['type', '=', 'out_invoice'], ['state', '=', 'open']]])
+        report = XMLRPC::Client.new2("#{url}/xmlrpc/2/report").proxy
+        result = report.render_report(
+            db, uid, password,
+            'account.report_invoice', invoice_ids)
+        report_data = Base64.decode64(result['result'])
+
+    .. code-block:: java
+
+        final Object[] invoice_ids = (Object[])models.execute(
+            "execute_kw", asList(
+                db, uid, password,
+                "account.invoice", "search",
+                asList(asList(
+                    asList("type", "=", "out_invoice"),
+                    asList("state", "=", "open")))
+        ));
+        final XmlRpcClientConfigImpl report_config = new XmlRpcClientConfigImpl();
+        report_config.setServerURL(
+            new URL(String.format("%s/xmlrpc/2/report", url)));
+        final Map<String, Object> result = (Map<String, Object>)client.execute(
+            report_config, "render_report", asList(
+                db, uid, password,
+                "account.report_invoice",
+                invoice_ids));
+        final byte[] report_data = DatatypeConverter.parseBase64Binary(
+            (String)result.get("result"));
+
+.. note::
+    :class: force-right
+
+    the report is sent as PDF binary data encoded in base64_, it must be
+    decoded and may need to be saved to disk before use
+
 .. _PostgreSQL: http://www.postgresql.org
 .. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
+.. _base64: http://en.wikipedia.org/wiki/Base64
index 72742f0..05ca907 100644 (file)
@@ -71,6 +71,8 @@ This means the first *group rule* restricts access, but any further
 
     although access rules do
 
+.. _reference/security/fields:
+
 Field Access
 ============
 
index 210213e..a6890f0 100644 (file)
@@ -284,6 +284,8 @@ defined (in addition to the Odoo ``safe_eval`` environment):
 - all the model column names, and
 - all the browse record's attributes.
 
+.. _reference/workflows/signals:
+
 Signals
 '''''''
 
index 43b63f8..27c0df0 100644 (file)
@@ -1057,8 +1057,8 @@ class Char(_String):
         return ustr(value)[:self.size]
 
 class Text(_String):
-    """ Text field. Very similar to :class:`~.Char` but used for longer
-     contents and displayed as a multiline text box
+    """ Very similar to :class:`~.Char` but used for longer contents, does not
+    have a size and usually displayed as a multiline text box.
 
     :param translate: whether the value of this field can be translated
     """
index 10ccb89..d5fde6c 100644 (file)
@@ -2994,8 +2994,8 @@ class BaseModel(object):
         elif 'x_name' in cls._fields:
             cls._rec_name = 'x_name'
 
-    def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
-        """ fields_get([fields])
+    def fields_get(self, cr, user, allfields=None, context=None, write_access=True, attributes=None):
+        """ fields_get([fields][, attributes])
 
         Return the definition of each field.
 
@@ -3003,16 +3003,14 @@ class BaseModel(object):
         dictionaries. The _inherits'd fields are included. The string, help,
         and selection (if present) attributes are translated.
 
-        :param cr: database cursor
-        :param user: current user id
-        :param allfields: list of fields
-        :param context: context arguments, like lang, time zone
-        :return: dictionary of field dictionaries, each one describing a field of the business object
-        :raise AccessError: * if user has no create/write rights on the requested object
-
+        :param allfields: list of fields to document, all if empty or not provided
+        :param attributes: list of description attributes to return for each field, all if empty or not provided
         """
         recs = self.browse(cr, user, [], context)
 
+        has_access = functools.partial(recs.check_access_rights, raise_exception=False)
+        readonly = not (has_access('write') or has_access('create'))
+
         res = {}
         for fname, field in self._fields.iteritems():
             if allfields and fname not in allfields:
@@ -3021,14 +3019,15 @@ class BaseModel(object):
                 continue
             if field.groups and not recs.user_has_groups(field.groups):
                 continue
-            res[fname] = field.get_description(recs.env)
 
-        # if user cannot create or modify records, make all fields readonly
-        has_access = functools.partial(recs.check_access_rights, raise_exception=False)
-        if not (has_access('write') or has_access('create')):
-            for description in res.itervalues():
+            description = field.get_description(recs.env)
+            if readonly:
                 description['readonly'] = True
                 description['states'] = {}
+            if attributes:
+                description = {k: v for k, v in description.iteritems()
+                               if k in attributes}
+            res[fname] = description
 
         return res
 
@@ -3604,38 +3603,67 @@ class BaseModel(object):
         :raise ValidateError: if user tries to enter invalid value for a field that is not in selection
         :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
 
-        .. _openerp/models/relationals/format:
-
-        .. note:: Relational fields use a special "commands" format to manipulate their values
-
-            This format is a list of command triplets executed sequentially,
-            possible command triplets are:
-
-            ``(0, _, values: dict)``
-                links to a new record created from the provided values
-            ``(1, id, values: dict)``
-                updates the already-linked record of id ``id`` with the
-                provided ``values``
-            ``(2, id, _)``
-                unlinks and deletes the linked record of id ``id``
-            ``(3, id, _)``
-                unlinks the linked record of id ``id`` without deleting it
-            ``(4, id, _)``
-                links to an existing record of id ``id``
-            ``(5, _, _)``
-                unlinks all records in the relation, equivalent to using
-                the command ``3`` on every linked record
-            ``(6, _, ids)``
-                replaces the existing list of linked records by the provoded
-                ones, equivalent to using ``5`` then ``4`` for each id in
-                ``ids``)
-
-            (in command triplets, ``_`` values are ignored and can be
-            anything, generally ``0`` or ``False``)
-
-            Any command can be used on :class:`~openerp.fields.Many2many`,
-            only ``0``, ``1`` and ``2`` can be used on
-            :class:`~openerp.fields.One2many`.
+        * For numeric fields (:class:`~openerp.fields.Integer`,
+          :class:`~openerp.fields.Float`) the value should be of the
+          corresponding type
+        * For :class:`~openerp.fields.Boolean`, the value should be a
+          :class:`python:bool`
+        * For :class:`~openerp.fields.Selection`, the value should match the
+          selection values (generally :class:`python:str`, sometimes
+          :class:`python:int`)
+        * For :class:`~openerp.fields.Many2one`, the value should be the
+          database identifier of the record to set
+        * Other non-relational fields use a string for value
+
+          .. danger::
+
+              for historical and compatibility reasons,
+              :class:`~openerp.fields.Date` and
+              :class:`~openerp.fields.Datetime` fields use strings as values
+              (written and read) rather than :class:`~python:datetime.date` or
+              :class:`~python:datetime.datetime`. These date strings are
+              UTC-only and formatted according to
+              :const:`openerp.tools.misc.DEFAULT_SERVER_DATE_FORMAT` and
+              :const:`openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT`
+        * .. _openerp/models/relationals/format:
+
+          :class:`~openerp.fields.One2many` and
+          :class:`~openerp.fields.Many2many` use a special "commands" format to
+          manipulate the set of records stored in/associated with the field.
+
+          This format is a list of triplets executed sequentially, where each
+          triplet is a command to execute on the set of records. Not all
+          commands apply in all situations. Possible commands are:
+
+          ``(0, _, values)``
+              adds a new record created from the provided ``value`` dict.
+          ``(1, id, values)``
+              updates an existing record of id ``id`` with the values in
+              ``values``. Can not be used in :meth:`~.create`.
+          ``(2, id, _)``
+              removes the record of id ``id`` from the set, then deletes it
+              (from the database). Can not be used in :meth:`~.create`.
+          ``(3, id, _)``
+              removes the record of id ``id`` from the set, but does not
+              delete it. Can not be used on
+              :class:`~openerp.fields.One2many`. Can not be used in
+              :meth:`~.create`.
+          ``(4, id, _)``
+              adds an existing record of id ``id`` to the set. Can not be
+              used on :class:`~openerp.fields.One2many`.
+          ``(5, _, _)``
+              removes all records from the set, equivalent to using the
+              command ``3`` on every record explicitly. Can not be used on
+              :class:`~openerp.fields.One2many`. Can not be used in
+              :meth:`~.create`.
+          ``(6, _, ids)``
+              replaces all existing records in the set by the ``ids`` list,
+              equivalent to using the command ``5`` followed by a command
+              ``4`` for each ``id`` in ``ids``. Can not be used on
+              :class:`~openerp.fields.One2many`.
+
+          .. note:: Values marked as ``_`` in the list above are ignored and
+                    can be anything, generally ``0`` or ``False``.
         """
         if not self:
             return True