From ec7736a0515c81f96154d7cabcefb69c19e6a57d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Nov 2014 08:52:38 +0100 Subject: [PATCH] [ADD] ws doc: introspection, reports and workflows * 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 | 746 +++++++++++++++++++++++++++++++----- doc/reference/security.rst | 2 + doc/reference/workflows.rst | 2 + openerp/fields.py | 4 +- openerp/models.py | 110 ++++-- 5 files changed, 731 insertions(+), 133 deletions(-) diff --git a/doc/api_integration.rst b/doc/api_integration.rst index 4a9835fa743..fef6dfa48d7 100644 --- a/doc/api_integration.rst +++ b/doc/api_integration.rst @@ -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 info = (Map)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 info = (Map)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 `_ + 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> fields = - (Map>)models.execute("execute_kw", Arrays.asList( - db, uid, password, - "res.partner", "fields_get", - Collections.emptyList())); - // filter keys of field attributes for display - final List allowed = Arrays.asList("string", "help", "type"); - new HashMap>() {{ - for(Entry> item: fields.entrySet()) { - put(item.getKey(), new HashMap() {{ - for(Entry it: item.getValue().entrySet()) { - if (allowed.contains(it.getKey())) { - put(it.getKey(), it.getValue()); - } - } - }}); - } - }}; + (Map>)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 `. + 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() {{ + put("name", "Custom Model"); + put("model", "x_custom_model"); + put("state", "manual"); + }}) + )); + final Object fields = models.execute( + "execute_kw", asList( + db, uid, password, + "x_custom_model", "fields_get", + emptyList(), + new HashMap () {{ + put("attributes", asList( + "string", + "help", + "type")); + }} + )); + +.. code-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 ` of field to create +``state`` + whether the field was created via Python code (``base``) or via + ``ir.model.fields`` (``manual``) +``required``, ``readonly``, ``translate`` + enables the corresponding flag on the field +``groups`` + :ref:`field-level access control `, a + :class:`~openerp.fields.Many2many` to ``res.groups`` +``selection``, ``size``, ``on_delete``, ``relation``, ``relation_field``, ``domain`` + type-specific properties and customizations, see :ref:`the fields + documentation ` for details + +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() {{ + put("name", "Custom Model"); + put("model", "x_custom"); + put("state", "manual"); + }}) + )); + models.execute( + "execute_kw", asList( + db, uid, password, + "ir.model.fields", "create", + asList(new HashMap() {{ + put("model_id", id); + put("name", "x_name"); + put("ttype", "char"); + put("state", "manual"); + put("required", true); + }}) + )); + final Integer record_id = (Integer)models.execute( + "execute_kw", asList( + db, uid, password, + "x_custom", "create", + asList(new HashMap() {{ + put("x_name", "test record"); + }}) + )); + + client.execute( + "execute_kw", asList( + db, uid, password, + "x_custom", "read", + asList(asList(record_id)) + )); + +.. code-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 c = (Map) + ((Object[])models.execute("execute_kw", asList( + db, uid, password, + "res.partner", "search_read", + asList( + asList( + asList("customer", "=", true))), + new HashMap() {{ + 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() {{ + 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() {{ + 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 result = (Map)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 diff --git a/doc/reference/security.rst b/doc/reference/security.rst index 72742f0639c..05ca9079fb7 100644 --- a/doc/reference/security.rst +++ b/doc/reference/security.rst @@ -71,6 +71,8 @@ This means the first *group rule* restricts access, but any further although access rules do +.. _reference/security/fields: + Field Access ============ diff --git a/doc/reference/workflows.rst b/doc/reference/workflows.rst index 210213e5f55..a6890f05ab6 100644 --- a/doc/reference/workflows.rst +++ b/doc/reference/workflows.rst @@ -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 ''''''' diff --git a/openerp/fields.py b/openerp/fields.py index 43b63f84b13..27c0df09a4b 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -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 """ diff --git a/openerp/models.py b/openerp/models.py index 10ccb8955b9..d5fde6c9fe5 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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: + * 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 - .. note:: Relational fields use a special "commands" format to manipulate their values + .. danger:: - This format is a list of command triplets executed sequentially, - possible command triplets are: + 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: - ``(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``) + :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. - (in command triplets, ``_`` values are ignored and can be - anything, generally ``0`` or ``False``) + 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: - Any command can be used on :class:`~openerp.fields.Many2many`, - only ``0``, ``1`` and ``2`` can be used on - :class:`~openerp.fields.One2many`. + ``(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