[MERGE] Forward-port of branch 12.0 to 13.0
@@ -0,0 +1,444 @@
|
||||
|
||||
.. _reference/actions:
|
||||
|
||||
=======
|
||||
Actions
|
||||
=======
|
||||
|
||||
Actions define the behavior of the system in response to user actions: login,
|
||||
action button, selection of an invoice, ...
|
||||
|
||||
Actions can be stored in the database or returned directly as dictionaries in
|
||||
e.g. button methods. All actions share two mandatory attributes:
|
||||
|
||||
``type``
|
||||
the category of the current action, determines which fields may be
|
||||
used and how the action is interpreted
|
||||
``name``
|
||||
short user-readable description of the action, may be displayed in the
|
||||
client's interface
|
||||
|
||||
A client can get actions in 4 forms:
|
||||
|
||||
* ``False``
|
||||
if any action dialog is currently open, close it
|
||||
* A string
|
||||
if a :ref:`client action <reference/actions/client>` matches, interpret as
|
||||
a client action's tag, otherwise treat as a number
|
||||
* A number
|
||||
read the corresponding action record from the database, may be a database
|
||||
identifier or an :term:`external id`
|
||||
* A dictionary
|
||||
treat as a client action descriptor and execute
|
||||
|
||||
.. _reference/bindings:
|
||||
|
||||
Bindings
|
||||
========
|
||||
|
||||
Aside from their two mandatory attributes, all actions also share *optional*
|
||||
attributes used to present an action in an arbitrary model's contextual menu:
|
||||
|
||||
``binding_model_id``
|
||||
specifies which model the action is bound to
|
||||
|
||||
.. note:: For Server Actions, use ``model_id``.
|
||||
|
||||
``binding_type``
|
||||
specifies the type of binding, which is mostly which contextual menu the
|
||||
action will appear under
|
||||
|
||||
``action`` (default)
|
||||
Specifies that the action will appear in the :menuselection:`Action`
|
||||
contextual menu of the bound model.
|
||||
``report``
|
||||
Specifies that the action will appear in the :menuselection:`Print`
|
||||
contextual menu of the bound model.
|
||||
``binding_view_types``
|
||||
a comma-separated list of view types for which the action appears in the
|
||||
contextual menu, mostly "list" and / or "form". Defaults to ``list,form``
|
||||
(both list and form )
|
||||
|
||||
.. _reference/actions/window:
|
||||
|
||||
Window Actions (``ir.actions.act_window``)
|
||||
==========================================
|
||||
|
||||
The most common action type, used to present visualisations of a model through
|
||||
:ref:`views <reference/views>`: a window action defines a set of view types
|
||||
(and possibly specific views) for a model (and possibly specific record of the
|
||||
model).
|
||||
|
||||
Its fields are:
|
||||
|
||||
``res_model``
|
||||
model to present views for
|
||||
``views``
|
||||
a list of ``(view_id, view_type)`` pairs. The second element of each pair
|
||||
is the category of the view (tree, form, graph, ...) and the first is
|
||||
an optional database id (or ``False``). If no id is provided, the client
|
||||
should fetch the default view of the specified type for the requested
|
||||
model (this is automatically done by
|
||||
:meth:`~odoo.models.Model.fields_view_get`). The first type of the
|
||||
list is the default view type and will be open by default when the action
|
||||
is executed. Each view type should be present at most once in the list
|
||||
``res_id`` (optional)
|
||||
if the default view is ``form``, specifies the record to load (otherwise
|
||||
a new record should be created)
|
||||
``search_view_id`` (optional)
|
||||
``(id, name)`` pair, ``id`` is the database identifier of a specific
|
||||
search view to load for the action. Defaults to fetching the default
|
||||
search view for the model
|
||||
``target`` (optional)
|
||||
whether the views should be open in the main content area (``current``),
|
||||
in full screen mode (``fullscreen``) or in a dialog/popup (``new``). Use
|
||||
``main`` instead of ``current`` to clear the breadcrumbs. Defaults to
|
||||
``current``.
|
||||
``context`` (optional)
|
||||
additional context data to pass to the views
|
||||
``domain`` (optional)
|
||||
filtering domain to implicitly add to all view search queries
|
||||
``limit`` (optional)
|
||||
number of records to display in lists by default. Defaults to 80 in the
|
||||
web client
|
||||
|
||||
For instance, to open customers (partner with the ``customer`` flag set) with
|
||||
list and form views::
|
||||
|
||||
{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "res.partner",
|
||||
"views": [[False, "tree"], [False, "form"]],
|
||||
"domain": [["customer", "=", true]],
|
||||
}
|
||||
|
||||
Or to open the form view of a specific product (obtained separately) in a new
|
||||
dialog::
|
||||
|
||||
{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "product.product",
|
||||
"views": [[False, "form"]],
|
||||
"res_id": a_product_id,
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
In-database window actions have a few different fields which should be ignored
|
||||
by clients, mostly to use in composing the ``views`` list:
|
||||
|
||||
``view_mode`` (default= ``tree,form`` )
|
||||
comma-separated list of view types as a string (/!\\ No spaces /!\\). All of these types will be
|
||||
present in the generated ``views`` list (with at least a ``False`` view_id)
|
||||
``view_ids``
|
||||
M2M\ [#notquitem2m]_ to view objects, defines the initial content of
|
||||
``views``
|
||||
|
||||
.. note:: Act_window views can also be defined cleanly through ``ir.actions.act_window.view``.
|
||||
|
||||
If you plan to allow multiple views for your model, prefer using
|
||||
ir.actions.act_window.view instead of the action ``view_ids``
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.act_window.view" id="test_action_tree">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_test_tree"/>
|
||||
<field name="act_window_id" ref="test_action"/>
|
||||
</record>
|
||||
|
||||
``view_id``
|
||||
specific view added to the ``views`` list in case its type is part of the
|
||||
``view_mode`` list and not already filled by one of the views in
|
||||
``view_ids``
|
||||
|
||||
These are mostly used when defining actions from :ref:`reference/data`:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.act_window" id="test_action">
|
||||
<field name="name">A Test Action</field>
|
||||
<field name="res_model">some.model</field>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="my_specific_view"/>
|
||||
</record>
|
||||
|
||||
will use the "my_specific_view" view even if that's not the default view for
|
||||
the model.
|
||||
|
||||
The server-side composition of the ``views`` sequence is the following:
|
||||
|
||||
* get each ``(id, type)`` from ``view_ids`` (ordered by ``sequence``)
|
||||
* if ``view_id`` is defined and its type isn't already filled, append its
|
||||
``(id, type)``
|
||||
* for each unfilled type in ``view_mode``, append ``(False, type)``
|
||||
|
||||
.. todo::
|
||||
|
||||
* ``usage``?
|
||||
* ``groups_id``?
|
||||
* ``filter``?
|
||||
|
||||
.. [#notquitem2m] technically not an M2M: adds a sequence field and may be
|
||||
composed of just a view type, without a view id.
|
||||
|
||||
.. _reference/actions/url:
|
||||
|
||||
URL Actions (``ir.actions.act_url``)
|
||||
====================================
|
||||
|
||||
Allow opening a URL (website/web page) via an Odoo action. Can be customized
|
||||
via two fields:
|
||||
|
||||
``url``
|
||||
the address to open when activating the action
|
||||
``target``
|
||||
opens the address in a new window/page if ``new``, replaces
|
||||
the current content with the page if ``self``. Defaults to ``new``
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "ir.actions.act_url",
|
||||
"url": "https://odoo.com",
|
||||
"target": "self",
|
||||
}
|
||||
|
||||
will replace the current content section by the Odoo home page.
|
||||
|
||||
.. _reference/actions/server:
|
||||
|
||||
Server Actions (``ir.actions.server``)
|
||||
======================================
|
||||
|
||||
.. autoclass:: odoo.addons.base.models.ir_actions.IrActionsServer
|
||||
|
||||
Allow triggering complex server code from any valid action location. Only
|
||||
two fields are relevant to clients:
|
||||
|
||||
``id``
|
||||
the in-database identifier of the server action to run
|
||||
``context`` (optional)
|
||||
context data to use when running the server action
|
||||
|
||||
In-database records are significantly richer and can perform a number of
|
||||
specific or generic actions based on their ``state``. Some fields (and
|
||||
corresponding behaviors) are shared between states:
|
||||
|
||||
``model_id``
|
||||
Odoo model linked to the action.
|
||||
|
||||
``state``
|
||||
|
||||
* ``code``: Executes python code given through the ``code`` argument.
|
||||
|
||||
* ``object_create``: Creates a new record of model ``crud_model_id`` following ``fields_lines`` specifications.
|
||||
|
||||
* ``object_write``: Updates the current record(s) following ``fields_lines`` specifications
|
||||
|
||||
* ``multi``: Executes serveral actions given through the ``child_ids`` argument.
|
||||
|
||||
State fields
|
||||
------------
|
||||
|
||||
Depending on its state, the behavior is defined through different fields.
|
||||
The concerned state is given after each field.
|
||||
|
||||
``code`` (code)
|
||||
Specify a piece of Python code to execute when the action is called
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.server" id="print_instance">
|
||||
<field name="name">Res Partner Server Action</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
raise Warning(record.name)
|
||||
</field>
|
||||
</record>
|
||||
|
||||
.. note::
|
||||
|
||||
The code segment can define a variable called ``action``, which will be
|
||||
returned to the client as the next action to execute:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.server" id="print_instance">
|
||||
<field name="name">Res Partner Server Action</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if record.some_condition():
|
||||
action = {
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": record._name,
|
||||
"res_id": record.id,
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
will ask the client to open a form for the record if it fulfills some
|
||||
condition
|
||||
|
||||
.. This tends to be the only action type created from :ref:`data files
|
||||
<reference/data>`, other types aside from
|
||||
:ref:`reference/actions/server/multi` are simpler than Python code to define
|
||||
from the UI, but not from :ref:`data files <reference/data>`.
|
||||
|
||||
``crud_model_id`` (create)(required)
|
||||
model in which to create a new record
|
||||
``link_field_id`` (create)
|
||||
many2one to ``ir.model.fields``, specifies the current record's m2o field
|
||||
on which the newly created record should be set (models should match)
|
||||
|
||||
``fields_lines`` (create/write)
|
||||
fields to override when creating or copying the record.
|
||||
:class:`~odoo.fields.One2many` with the fields:
|
||||
|
||||
``col1``
|
||||
``ir.model.fields`` to set in the concerned model
|
||||
(``crud_model_id`` for creates, ``model_id`` for updates)
|
||||
``value``
|
||||
value for the field, interpreted via ``type``
|
||||
``type`` (value|reference|equation)
|
||||
If ``value``, the ``value`` field is interpreted as a literal value
|
||||
(possibly converted), if ``equation`` the ``value`` field is
|
||||
interpreted as a Python expression and evaluated
|
||||
|
||||
``child_ids`` (multi)
|
||||
Specify the multiple sub-actions (``ir.actions.server``) to enact in state multi.
|
||||
If sub-actions themselves return actions, the last
|
||||
one will be returned to the client as the multi's own next action
|
||||
|
||||
.. _reference/actions/server/context:
|
||||
|
||||
Evaluation context
|
||||
------------------
|
||||
|
||||
A number of keys are available in the evaluation context of or surrounding
|
||||
server actions:
|
||||
|
||||
* ``model`` model object linked to the action via ``model_id``
|
||||
* ``record``/``records`` record/recorset on which the action is triggered, can be void.
|
||||
* ``env`` Odoo Environment
|
||||
* ``datetime``, ``dateutil``, ``time``, ``timezone`` corresponding Python modules
|
||||
* ``log: log(message, level='info')`` logging function to record debug information in ir.logging table
|
||||
* ``Warning`` constructor for the ``Warning`` exception
|
||||
|
||||
.. _reference/actions/report:
|
||||
|
||||
Report Actions (``ir.actions.report``)
|
||||
======================================
|
||||
|
||||
Triggers the printing of a report.
|
||||
|
||||
If you define your report through a `<record>` instead of a `<report>` tag and
|
||||
want the action to show up in the Print menu of the model's views, you will
|
||||
also need to specify ``binding_model_id`` from :ref:`reference/bindings`. It's
|
||||
not necessary to set ``binding_type`` to ``report``, since
|
||||
``ir.actions.report`` will implicitly default to that.
|
||||
|
||||
|
||||
``name`` (mandatory)
|
||||
used as the file name if ``print_report_name`` is not specified.
|
||||
Otherwise, only useful as a mnemonic/description of the report
|
||||
when looking for one in a list of some sort
|
||||
``model`` (mandatory)
|
||||
the model your report will be about
|
||||
``report_type`` (default=qweb-pdf)
|
||||
either ``qweb-pdf`` for PDF reports or ``qweb-html`` for HTML
|
||||
``report_name`` (mandatory)
|
||||
the name (:term:`external id`) of the qweb template used to render the report
|
||||
``print_report_name``
|
||||
python expression defining the name of the report.
|
||||
``groups_id``
|
||||
:class:`~odoo.fields.Many2many` field to the groups allowed to view/use
|
||||
the current report
|
||||
``multi``
|
||||
if set to ``True``, the action will not be displayed on a form view.
|
||||
``paperformat_id``
|
||||
:class:`~odoo.fields.Many2one` field to the paper format you wish to
|
||||
use for this report (if not specified, the company format will be used)
|
||||
``attachment_use``
|
||||
if set to ``True``, the report is only generated once the first time it is
|
||||
requested, and re-printed from the stored report afterwards instead of
|
||||
being re-generated every time.
|
||||
|
||||
Can be used for reports which must only be generated once (e.g. for legal
|
||||
reasons)
|
||||
``attachment``
|
||||
python expression that defines the name of the report; the record is
|
||||
accessible as the variable ``object``
|
||||
|
||||
.. _reference/actions/client:
|
||||
|
||||
Client Actions (``ir.actions.client``)
|
||||
======================================
|
||||
|
||||
Triggers an action implemented entirely in the client.
|
||||
|
||||
``tag``
|
||||
the client-side identifier of the action, an arbitrary string which
|
||||
the client should know how to react to
|
||||
``params`` (optional)
|
||||
a Python dictionary of additional data to send to the client, alongside
|
||||
the client action tag
|
||||
``target`` (optional)
|
||||
whether the client action should be open in the main content area
|
||||
(``current``), in full screen mode (``fullscreen``) or in a dialog/popup
|
||||
(``new``). Use ``main`` instead of ``current`` to clear the breadcrumbs.
|
||||
Defaults to ``current``.
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"type": "ir.actions.client",
|
||||
"tag": "pos.ui"
|
||||
}
|
||||
|
||||
tells the client to start the Point of Sale interface, the server has no idea
|
||||
how the POS interface works.
|
||||
|
||||
.. seealso::
|
||||
- :ref:`Tutorial: Client Actions <howtos/web/client_actions>`
|
||||
|
||||
.. _reference/actions/cron:
|
||||
|
||||
Automated Actions (``ir.cron``)
|
||||
======================================
|
||||
|
||||
Actions triggered automatically on a predefined frequency.
|
||||
|
||||
``name``
|
||||
Name of the automated action (Mainly used in log display)
|
||||
|
||||
``interval_number``
|
||||
Number of *interval_type* uom between two executions of the action
|
||||
|
||||
``interval_type``
|
||||
Unit of measure of frequency interval (``minutes``, ``hours``, ``days``, ``weeks``, ``months``,
|
||||
|
||||
``numbercall``
|
||||
Number of times this action has to be run.
|
||||
If the action is expected to run indefinitely, set to ``-1``.
|
||||
|
||||
``doall``
|
||||
Boolean precising whether the missed actions have to be executed in case of
|
||||
server restarts.
|
||||
|
||||
``model_id``
|
||||
Model on which this action will be called
|
||||
|
||||
``code``
|
||||
Code content of the action.
|
||||
Can be a simple call to the model's method :
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
model.<method_name>()
|
||||
|
||||
``nextcall``
|
||||
Next planned execution date of this action (date/time format)
|
||||
@@ -0,0 +1,704 @@
|
||||
|
||||
.. _reference/cmdline:
|
||||
|
||||
================================
|
||||
Command-line interface: odoo-bin
|
||||
================================
|
||||
|
||||
.. _reference/cmdline/server:
|
||||
|
||||
Running the server
|
||||
==================
|
||||
|
||||
.. program:: odoo-bin
|
||||
|
||||
.. option:: -d <database>, --database <database>
|
||||
|
||||
database(s) used when installing or updating modules.
|
||||
Providing a comma-separated list restrict access to databases provided in
|
||||
list.
|
||||
|
||||
For advanced database options, take a look :ref:`below <reference/cmdline/server/database>`.
|
||||
|
||||
.. option:: -i <modules>, --init <modules>
|
||||
|
||||
comma-separated list of modules to install before running the server
|
||||
(requires :option:`-d`).
|
||||
|
||||
.. option:: -u <modules>, --update <modules>
|
||||
|
||||
comma-separated list of modules to update before running the server
|
||||
(requires :option:`-d`).
|
||||
|
||||
.. option:: --addons-path <directories>
|
||||
|
||||
comma-separated list of directories in which modules are stored. These
|
||||
directories are scanned for modules.
|
||||
|
||||
.. (nb: when and why?)
|
||||
|
||||
.. option:: -c <config>, --config <config>
|
||||
|
||||
provide an alternate :ref:`configuration file <reference/cmdline/config>`
|
||||
|
||||
.. option:: -s, --save
|
||||
|
||||
saves the server configuration to the current configuration file
|
||||
(:file:`{$HOME}/.odoorc` by default, and can be overridden using
|
||||
:option:`-c`).
|
||||
|
||||
.. option:: --without-demo
|
||||
|
||||
disables demo data loading for modules installed
|
||||
comma-separated, use ``all`` for all modules.
|
||||
|
||||
.. option:: --test-enable
|
||||
|
||||
runs tests after installing modules
|
||||
|
||||
.. option:: --test-tags 'tag_1,tag_2,...,-tag_n'
|
||||
|
||||
select the tests to run by using tags.
|
||||
|
||||
.. option:: --screenshots
|
||||
|
||||
Specify directory where to write screenshots when an HttpCase.browser_js test
|
||||
fails. It defaults to :file:`/tmp/odoo_tests/{db_name}/screenshots`
|
||||
|
||||
.. option:: --screencasts
|
||||
|
||||
Enable screencasts and specify directory where to write screencasts files.
|
||||
The ``ffmpeg`` utility needs to be installed to encode frames into a video
|
||||
file. Otherwise frames will be kept instead of the video file.
|
||||
|
||||
.. _reference/cmdline/server/database:
|
||||
|
||||
Database
|
||||
--------
|
||||
|
||||
.. option:: -r <user>, --db_user <user>
|
||||
|
||||
database username, used to connect to PostgreSQL.
|
||||
|
||||
.. option:: -w <password>, --db_password <password>
|
||||
|
||||
database password, if using `password authentication`_.
|
||||
|
||||
.. option:: --db_host <hostname>
|
||||
|
||||
host for the database server
|
||||
|
||||
* ``localhost`` on Windows
|
||||
* UNIX socket otherwise
|
||||
|
||||
.. option:: --db_port <port>
|
||||
|
||||
port the database listens on, defaults to 5432
|
||||
|
||||
.. option:: --db-filter <filter>
|
||||
|
||||
hides databases that do not match ``<filter>``. The filter is a
|
||||
`regular expression`_, with the additions that:
|
||||
|
||||
- ``%h`` is replaced by the whole hostname the request is made on.
|
||||
- ``%d`` is replaced by the subdomain the request is made on, with the
|
||||
exception of ``www`` (so domain ``odoo.com`` and ``www.odoo.com`` both
|
||||
match the database ``odoo``).
|
||||
|
||||
These operations are case sensitive. Add option ``(?i)`` to match all
|
||||
databases (so domain ``odoo.com`` using ``(?i)%d`` matches the database
|
||||
``Odoo``).
|
||||
|
||||
Since version 11, it's also possible to restrict access to a given database
|
||||
listen by using the --database parameter and specifying a comma-separated
|
||||
list of databases
|
||||
|
||||
When combining the two parameters, db-filter supersedes the comma-separated
|
||||
database list for restricting database list, while the comma-separated list
|
||||
is used for performing requested operations like upgrade of modules.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ odoo-bin --db-filter ^11.*$
|
||||
|
||||
Restrict access to databases whose name starts with 11
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ odoo-bin --database 11firstdatabase,11seconddatabase
|
||||
|
||||
Restrict access to only two databases, 11firstdatabase and 11seconddatabase
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ odoo-bin --database 11firstdatabase,11seconddatabase -u base
|
||||
|
||||
Restrict access to only two databases, 11firstdatabase and 11seconddatabase,
|
||||
and update base module on one database: 11firstdatabase.
|
||||
If database 11seconddatabase doesn't exist, the database is created and base modules
|
||||
is installed
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ odoo-bin --db-filter ^11.*$ --database 11firstdatabase,11seconddatabase -u base
|
||||
|
||||
Restrict access to databases whose name starts with 11,
|
||||
and update base module on one database: 11firstdatabase.
|
||||
If database 11seconddatabase doesn't exist, the database is created and base modules
|
||||
is installed
|
||||
|
||||
.. option:: --db-template <template>
|
||||
|
||||
when creating new databases from the database-management screens, use the
|
||||
specified `template database`_. Defaults to ``template0``.
|
||||
|
||||
.. option:: --pg_path </path/to/postgresql/binaries>
|
||||
|
||||
Path to the PostgreSQL binaries that are used by the database manager to
|
||||
dump and restore databases. You have to specify this option only if these
|
||||
binaries are located in a non-standard directory.
|
||||
|
||||
.. option:: --no-database-list
|
||||
|
||||
Suppresses the ability to list databases available on the system
|
||||
|
||||
.. option:: --db_sslmode
|
||||
|
||||
Control the SSL security of the connection between Odoo and PostgreSQL.
|
||||
Value should bve one of 'disable', 'allow', 'prefer', 'require',
|
||||
'verify-ca' or 'verify-full'
|
||||
Default value is 'prefer'
|
||||
|
||||
.. _reference/cmdline/server/emails:
|
||||
|
||||
Emails
|
||||
------
|
||||
|
||||
.. option:: --email-from <address>
|
||||
|
||||
Email address used as <FROM> when Odoo needs to send mails
|
||||
|
||||
.. option:: --smtp <server>
|
||||
|
||||
Address of the SMTP server to connect to in order to send mails
|
||||
|
||||
.. option:: --smtp-port <port>
|
||||
|
||||
.. option:: --smtp-ssl
|
||||
|
||||
If set, odoo should use SSL/STARTSSL SMTP connections
|
||||
|
||||
.. option:: --smtp-user <name>
|
||||
|
||||
Username to connect to the SMTP server
|
||||
|
||||
.. option:: --smtp-password <password>
|
||||
|
||||
Password to connect to the SMTP server
|
||||
|
||||
.. _reference/cmdline/server/internationalisation:
|
||||
|
||||
Internationalisation
|
||||
--------------------
|
||||
|
||||
Use these options to translate Odoo to another language. See i18n section of
|
||||
the user manual. Option '-d' is mandatory. Option '-l' is mandatory in case
|
||||
of importation
|
||||
|
||||
.. option:: --load-language <languages>
|
||||
|
||||
specifies the languages (separated by commas) for the translations you
|
||||
want to be loaded
|
||||
|
||||
.. option:: -l, --language <language>
|
||||
|
||||
specify the language of the translation file. Use it with --i18n-export
|
||||
or --i18n-import
|
||||
|
||||
.. option:: --i18n-export <filename>
|
||||
|
||||
export all sentences to be translated to a CSV file, a PO file or a TGZ
|
||||
archive and exit.
|
||||
|
||||
.. option:: --i18n-import <filename>
|
||||
|
||||
import a CSV or a PO file with translations and exit. The '-l' option is
|
||||
required.
|
||||
|
||||
.. option:: --i18n-overwrite
|
||||
|
||||
overwrites existing translation terms on updating a module or importing
|
||||
a CSV or a PO file.
|
||||
|
||||
.. option:: --modules
|
||||
|
||||
specify modules to export. Use in combination with --i18n-export
|
||||
|
||||
.. _reference/cmdline/advanced:
|
||||
|
||||
Advanced Options
|
||||
----------------
|
||||
|
||||
.. _reference/cmdline/dev:
|
||||
|
||||
Developer features
|
||||
''''''''''''''''''
|
||||
|
||||
.. option:: --dev <feature,feature,...,feature>
|
||||
|
||||
* ``all``: all the features below are activated
|
||||
|
||||
* ``xml``: read template qweb from xml file directly instead of database.
|
||||
Once a template has been modified in database, it will be not be read from
|
||||
the xml file until the next update/init.
|
||||
|
||||
* ``reload``: restart server when python file are updated (may not be detected
|
||||
depending on the text editor used)
|
||||
|
||||
* ``qweb``: break in the evaluation of qweb template when a node contains ``t-debug='debugger'``
|
||||
|
||||
* ``(i)p(u)db``: start the chosen python debugger in the code when an
|
||||
unexpected error is raised before logging and returning the error.
|
||||
|
||||
|
||||
.. _reference/cmdline/server/http:
|
||||
|
||||
HTTP
|
||||
''''
|
||||
|
||||
.. option:: --no-http
|
||||
|
||||
do not start the HTTP or long-polling workers (may still start :ref:`cron <reference/actions/cron>`
|
||||
workers)
|
||||
|
||||
.. warning:: has no effect if :option:`--test-enable` is set, as tests
|
||||
require an accessible HTTP server
|
||||
|
||||
.. option:: --http-interface <interface>
|
||||
|
||||
TCP/IP address on which the HTTP server listens, defaults to ``0.0.0.0``
|
||||
(all addresses)
|
||||
|
||||
.. option:: --http-port <port>
|
||||
|
||||
Port on which the HTTP server listens, defaults to 8069.
|
||||
|
||||
.. option:: --longpolling-port <port>
|
||||
|
||||
TCP port for long-polling connections in multiprocessing or gevent mode,
|
||||
defaults to 8072. Not used in default (threaded) mode.
|
||||
|
||||
.. option:: --proxy-mode
|
||||
|
||||
enables the use of ``X-Forwarded-*`` headers through `Werkzeug's proxy
|
||||
support`_.
|
||||
|
||||
.. warning:: proxy mode *must not* be enabled outside of a reverse proxy
|
||||
scenario
|
||||
|
||||
.. _reference/cmdline/server/logging:
|
||||
|
||||
Logging
|
||||
'''''''
|
||||
|
||||
By default, Odoo displays all logging of level_ ``info`` except for workflow
|
||||
logging (``warning`` only), and log output is sent to ``stdout``. Various
|
||||
options are available to redirect logging to other destinations and to
|
||||
customize the amount of logging output.
|
||||
|
||||
.. option:: --logfile <file>
|
||||
|
||||
sends logging output to the specified file instead of stdout. On Unix, the
|
||||
file `can be managed by external log rotation programs
|
||||
<https://docs.python.org/3/library/logging.handlers.html#watchedfilehandler>`_
|
||||
and will automatically be reopened when replaced
|
||||
|
||||
.. option:: --syslog
|
||||
|
||||
logs to the system's event logger: `syslog on unices <https://docs.python.org/3/library/logging.handlers.html#sysloghandler>`_
|
||||
and `the Event Log on Windows <https://docs.python.org/3/library/logging.handlers.html#nteventloghandler>`_.
|
||||
|
||||
Neither is configurable
|
||||
|
||||
.. option:: --log-db <dbname>
|
||||
|
||||
logs to the ``ir.logging`` model (``ir_logging`` table) of the specified
|
||||
database. The database can be the name of a database in the "current"
|
||||
PostgreSQL, or `a PostgreSQL URI`_ for e.g. log aggregation.
|
||||
|
||||
.. option:: --log-handler <handler-spec>
|
||||
|
||||
:samp:`{LOGGER}:{LEVEL}`, enables ``LOGGER`` at the provided ``LEVEL``
|
||||
e.g. ``odoo.models:DEBUG`` will enable all logging messages at or above
|
||||
``DEBUG`` level in the models.
|
||||
|
||||
* The colon ``:`` is mandatory
|
||||
* The logger can be omitted to configure the root (default) handler
|
||||
* If the level is omitted, the logger is set to ``INFO``
|
||||
|
||||
The option can be repeated to configure multiple loggers e.g.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --log-handler :DEBUG --log-handler werkzeug:CRITICAL --log-handler odoo.fields:WARNING
|
||||
|
||||
.. option:: --log-request
|
||||
|
||||
enable DEBUG logging for RPC requests, equivalent to
|
||||
``--log-handler=odoo.http.rpc.request:DEBUG``
|
||||
|
||||
.. option:: --log-response
|
||||
|
||||
enable DEBUG logging for RPC responses, equivalent to
|
||||
``--log-handler=odoo.http.rpc.response:DEBUG``
|
||||
|
||||
.. option:: --log-web
|
||||
|
||||
enables DEBUG logging of HTTP requests and responses, equivalent to
|
||||
``--log-handler=odoo.http:DEBUG``
|
||||
|
||||
.. option:: --log-sql
|
||||
|
||||
enables DEBUG logging of SQL querying, equivalent to
|
||||
``--log-handler=odoo.sql_db:DEBUG``
|
||||
|
||||
.. option:: --log-level <level>
|
||||
|
||||
Shortcut to more easily set predefined levels on specific loggers. "real"
|
||||
levels (``critical``, ``error``, ``warn``, ``debug``) are set on the
|
||||
``odoo`` and ``werkzeug`` loggers (except for ``debug`` which is only
|
||||
set on ``odoo``).
|
||||
|
||||
Odoo also provides debugging pseudo-levels which apply to different sets
|
||||
of loggers:
|
||||
|
||||
``debug_sql``
|
||||
sets the SQL logger to ``debug``
|
||||
|
||||
equivalent to ``--log-sql``
|
||||
``debug_rpc``
|
||||
sets the ``odoo`` and HTTP request loggers to ``debug``
|
||||
|
||||
equivalent to ``--log-level debug --log-request``
|
||||
``debug_rpc_answer``
|
||||
sets the ``odoo`` and HTTP request and response loggers to
|
||||
``debug``
|
||||
|
||||
equivalent to ``--log-level debug --log-request --log-response``
|
||||
|
||||
.. note::
|
||||
|
||||
In case of conflict between :option:`--log-level` and
|
||||
:option:`--log-handler`, the latter is used
|
||||
|
||||
.. _reference/cdmline/workers:
|
||||
|
||||
Multiprocessing
|
||||
'''''''''''''''
|
||||
|
||||
.. option:: --workers <count>
|
||||
|
||||
if ``count`` is not 0 (the default), enables multiprocessing and sets up
|
||||
the specified number of HTTP workers (sub-processes processing HTTP
|
||||
and RPC requests).
|
||||
|
||||
.. note:: multiprocessing mode is only available on Unix-based systems
|
||||
|
||||
A number of options allow limiting and recycling workers:
|
||||
|
||||
.. option:: --limit-request <limit>
|
||||
|
||||
Number of requests a worker will process before being recycled and
|
||||
restarted.
|
||||
|
||||
Defaults to *8196*.
|
||||
|
||||
.. option:: --limit-memory-soft <limit>
|
||||
|
||||
Maximum allowed virtual memory per worker. If the limit is exceeded,
|
||||
the worker is killed and recycled at the end of the current request.
|
||||
|
||||
Defaults to *2048MiB*.
|
||||
|
||||
.. option:: --limit-memory-hard <limit>
|
||||
|
||||
Hard limit on virtual memory, any worker exceeding the limit will be
|
||||
immediately killed without waiting for the end of the current request
|
||||
processing.
|
||||
|
||||
Defaults to *2560MiB*.
|
||||
|
||||
.. option:: --limit-time-cpu <limit>
|
||||
|
||||
Prevents the worker from using more than <limit> CPU seconds for each
|
||||
request. If the limit is exceeded, the worker is killed.
|
||||
|
||||
Defaults to *60*.
|
||||
|
||||
.. option:: --limit-time-real <limit>
|
||||
|
||||
Prevents the worker from taking longer than <limit> seconds to process
|
||||
a request. If the limit is exceeded, the worker is killed.
|
||||
|
||||
Differs from :option:`--limit-time-cpu` in that this is a "wall time"
|
||||
limit including e.g. SQL queries.
|
||||
|
||||
Defaults to *120*.
|
||||
|
||||
.. option:: --max-cron-threads <count>
|
||||
|
||||
number of workers dedicated to :ref:`cron <reference/actions/cron>` jobs. Defaults to *2*.
|
||||
The workers are threads in multi-threading mode and processes in multi-processing mode.
|
||||
|
||||
For multi-processing mode, this is in addition to the HTTP worker processes.
|
||||
|
||||
.. _reference/cmdline/config:
|
||||
|
||||
Configuration file
|
||||
==================
|
||||
|
||||
.. program:: odoo-bin
|
||||
|
||||
Most of the command-line options can also be specified via a configuration
|
||||
file. Most of the time, they use similar names with the prefix ``-`` removed
|
||||
and other ``-`` are replaced by ``_`` e.g. :option:`--db-template` becomes
|
||||
``db_template``.
|
||||
|
||||
Some conversions don't match the pattern:
|
||||
|
||||
* :option:`--db-filter` becomes ``dbfilter``
|
||||
* :option:`--no-http` corresponds to the ``http_enable`` boolean
|
||||
* logging presets (all options starting with ``--log-`` except for
|
||||
:option:`--log-handler` and :option:`--log-db`) just add content to
|
||||
``log_handler``, use that directly in the configuration file
|
||||
* :option:`--smtp` is stored as ``smtp_server``
|
||||
* :option:`--database` is stored as ``db_name``
|
||||
* :option:`--i18n-import` and :option:`--i18n-export` aren't available at all
|
||||
from configuration files
|
||||
|
||||
The default configuration file is :file:`{$HOME}/.odoorc` which
|
||||
can be overridden using :option:`--config <odoo-bin -c>`. Specifying
|
||||
:option:`--save <odoo-bin -s>` will save the current configuration state back
|
||||
to that file.
|
||||
|
||||
.. _jinja2: http://jinja.pocoo.org
|
||||
.. _regular expression: https://docs.python.org/3/library/re.html
|
||||
.. _password authentication:
|
||||
https://www.postgresql.org/docs/9.3/static/auth-methods.html#AUTH-PASSWORD
|
||||
.. _template database:
|
||||
https://www.postgresql.org/docs/9.3/static/manage-ag-templatedbs.html
|
||||
.. _level:
|
||||
https://docs.python.org/3/library/logging.html#logging.Logger.setLevel
|
||||
.. _a PostgreSQL URI:
|
||||
https://www.postgresql.org/docs/9.2/static/libpq-connect.html#AEN38208
|
||||
.. _Werkzeug's proxy support:
|
||||
http://werkzeug.pocoo.org/docs/contrib/fixers/#werkzeug.contrib.fixers.ProxyFix
|
||||
.. _pyinotify: https://github.com/seb-m/pyinotify/wiki
|
||||
|
||||
|
||||
Shell
|
||||
=====
|
||||
|
||||
Odoo command-line also allows to launch odoo as a python console environment.
|
||||
This enables direct interaction with the :ref:`orm <reference/orm>` and its functionalities.
|
||||
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo_bin shell
|
||||
|
||||
.. option:: --shell-interface (ipython|ptpython|bpython|python)
|
||||
|
||||
Specify a preferred REPL to use in shell mode.
|
||||
|
||||
|
||||
.. _reference/cmdline/scaffold:
|
||||
|
||||
Scaffolding
|
||||
===========
|
||||
|
||||
.. program:: odoo-bin scaffold
|
||||
|
||||
Scaffolding is the automated creation of a skeleton structure to simplify
|
||||
bootstrapping (of new modules, in the case of Odoo). While not necessary it
|
||||
avoids the tedium of setting up basic structures and looking up what all
|
||||
starting requirements are.
|
||||
|
||||
Scaffolding is available via the :command:`odoo-bin scaffold` subcommand.
|
||||
|
||||
.. option:: name (required)
|
||||
|
||||
the name of the module to create, may munged in various manners to
|
||||
generate programmatic names (e.g. module directory name, model names, …)
|
||||
|
||||
.. option:: destination (default=current directory)
|
||||
|
||||
directory in which to create the new module, defaults to the current
|
||||
directory
|
||||
|
||||
.. option:: -t <template>
|
||||
|
||||
a template directory, files are passed through jinja2_ then copied to
|
||||
the ``destination`` directory
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo_bin scaffold my_module /addons/
|
||||
|
||||
This will create module *my_module* in directory */addons/*.
|
||||
|
||||
|
||||
Cloc
|
||||
====
|
||||
|
||||
.. program:: odoo-bin cloc
|
||||
|
||||
Odoo Cloc is a tool to count the number of relevant lines written in
|
||||
Python, Javascript or XML. This can be used as a rough metric for pricing
|
||||
maintenance of extra modules.
|
||||
|
||||
Command-line options
|
||||
--------------------
|
||||
.. option:: -d <database>, --database <database>
|
||||
|
||||
| Process the code of all extra modules installed on the provided database,
|
||||
and of all server actions and computed fields manually created in the provided
|
||||
database.
|
||||
| The :option:`--addons-path` option is required to specify the path(s) to the
|
||||
module folder(s).
|
||||
| If combined with :option:`--path`, the count will be that of the sum of both
|
||||
options' results (with possible overlaps). At least one of these two options is
|
||||
required to specify which code to process.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin cloc --addons-path=addons -d my_database
|
||||
|
||||
.. seealso::
|
||||
- :ref:`reference/cmdline/cloc/database-option`
|
||||
|
||||
|
||||
.. option:: -p <path>, --path <path>
|
||||
|
||||
| Process the files in the provided path.
|
||||
| If combined with :option:`--database`, the count will be that of the sum of both
|
||||
options' results (with possible overlaps). At least one of these two options is
|
||||
required to specify which code to process.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin cloc -p addons/account
|
||||
|
||||
|
||||
Multiple paths can be provided by repeating the option.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin cloc -p addons/account -p addons/sale
|
||||
|
||||
.. seealso::
|
||||
- :ref:`reference/cmdline/cloc/path-option`
|
||||
|
||||
|
||||
.. option:: --addons-path <directories>
|
||||
|
||||
| Comma-separated list of directories in which modules are stored. These directories
|
||||
are scanned for modules.
|
||||
| Required if the :option:`--database` option is used.
|
||||
|
||||
|
||||
.. option:: -c <directories>
|
||||
|
||||
Specify a configuration file to use in place of the :option:`--addons-path` option.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin cloc -c config.conf -d my_database
|
||||
|
||||
|
||||
.. option:: -v, --verbose
|
||||
|
||||
Show the details of lines counted for each file.
|
||||
|
||||
|
||||
Processed files
|
||||
---------------
|
||||
|
||||
.. _reference/cmdline/cloc/database-option:
|
||||
|
||||
With the :option:`--database` option
|
||||
''''''''''''''''''''''''''''''''''''
|
||||
|
||||
Odoo Cloc counts the lines in each file of extra installed modules in a
|
||||
given database. In addition, it counts the Python lines of server actions and
|
||||
custom computed fields that have been directly created in the database or
|
||||
imported.
|
||||
|
||||
Some files are excluded from the count by default:
|
||||
|
||||
- The manifest (:file:`__manifest__.py` or :file:`__openerp__.py`)
|
||||
- The contents of the folder :file:`static/lib`
|
||||
- The tests defined in the folder :file:`tests` and :file:`static/tests`
|
||||
- The migrations scripts defined in the folder :file:`migrations`
|
||||
- The XML files declared in the ``demo`` or ``demo_xml`` sections of the manifest
|
||||
|
||||
For special cases, a list of files that should be ignored by Odoo Cloc can be defined
|
||||
per module. This is specified by the ``cloc_exclude`` entry of the manifest:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"cloc_exclude": [
|
||||
"lib/common.py", # exclude a single file
|
||||
"data/*.xml", # exclude all XML files in a specific folder
|
||||
"example/**/*", # exclude all files in a folder hierarchy recursively
|
||||
]
|
||||
|
||||
| The pattern ``**/*`` can be used to ignore an entire module. This can be useful
|
||||
to exclude a module from maintenance service costs.
|
||||
| For more information about the pattern syntax, see `glob
|
||||
<https://docs.python.org/3/library/pathlib.html#pathlib.Path.glob>`_.
|
||||
|
||||
|
||||
|
||||
.. _reference/cmdline/cloc/path-option:
|
||||
|
||||
With the :option:`--path` option
|
||||
''''''''''''''''''''''''''''''''
|
||||
|
||||
This method works the same as with the :ref:`--database option
|
||||
<reference/cmdline/cloc/database-option>` if a manifest file is present in the given
|
||||
folder. Otherwise, it counts all files.
|
||||
|
||||
|
||||
Identifying Extra Modules
|
||||
-------------------------
|
||||
|
||||
To distinguish between standard and extra modules, Odoo Cloc uses the following heuristic:
|
||||
modules that are located (real file system path, after following symbolic links)
|
||||
in the same parent directory as the ``base``, ``web`` or ``web_enterprise``
|
||||
standard modules are considered standard. Other modules are treated as extra modules.
|
||||
|
||||
|
||||
Error Handling
|
||||
--------------
|
||||
|
||||
Some file cannot be counted by Odoo Cloc.
|
||||
Those file are reported at the end of the output.
|
||||
|
||||
Max file size exceeded
|
||||
''''''''''''''''''''''
|
||||
|
||||
Odoo Cloc rejects any file larger than 25MB. Usually, source files are smaller
|
||||
than 1 MB. If a file is rejected, it may be:
|
||||
|
||||
- A generated XML file that contains lots of data. It should be excluded in the manifest.
|
||||
- A JavaScript library that should be placed in the :file:`static/lib` folder.
|
||||
|
||||
Syntax Error
|
||||
''''''''''''
|
||||
|
||||
Odoo Cloc cannot count the lines of code of a Python file with a syntax problem.
|
||||
If an extra module contains such files, they should be fixed to allow the module to
|
||||
load. If the module works despite the presence of those files, they are probably
|
||||
not loaded and should therefore be removed from the module, or at least excluded
|
||||
in the manifest via ``cloc_exclude``.
|
||||
@@ -0,0 +1,316 @@
|
||||
|
||||
.. _reference/data:
|
||||
|
||||
==========
|
||||
Data Files
|
||||
==========
|
||||
|
||||
Odoo is greatly data-driven, and a big part of modules definition is thus
|
||||
the definition of the various records it manages: UI (menus and views),
|
||||
security (access rights and access rules), reports and plain data are all
|
||||
defined via records.
|
||||
|
||||
Structure
|
||||
=========
|
||||
|
||||
The main way to define data in Odoo is via XML data files: The broad structure
|
||||
of an XML data file is the following:
|
||||
|
||||
* Any number of operation elements within the root element ``odoo``
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<!-- the root elements of the data file -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<operation/>
|
||||
...
|
||||
</odoo>
|
||||
|
||||
Data files are executed sequentially, operations can only refer to the result
|
||||
of operations defined previously
|
||||
|
||||
.. note::
|
||||
|
||||
If the content of the data file is expected to be applied only once, you
|
||||
can specify the odoo flag ``noupdate`` set to 1. If part of
|
||||
the data in the file is expected to be applied once, you can place this part
|
||||
of the file in a <data noupdate="1"> domain.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Only loaded when installing the module (odoo-bin -i module) -->
|
||||
<operation/>
|
||||
</data>
|
||||
|
||||
<!-- (Re)Loaded at install and update (odoo-bin -i/-u) -->
|
||||
<operation/>
|
||||
</odoo>
|
||||
|
||||
Core operations
|
||||
===============
|
||||
|
||||
.. _reference/data/record:
|
||||
|
||||
``record``
|
||||
----------
|
||||
|
||||
``record`` appropriately defines or updates a database record, it has the
|
||||
following attributes:
|
||||
|
||||
``model`` (required)
|
||||
name of the model to create (or update)
|
||||
``id``
|
||||
the :term:`external identifier` for this record. It is strongly
|
||||
recommended to provide one
|
||||
|
||||
* for record creation, allows subsequent definitions to either modify or
|
||||
refer to this record
|
||||
* for record modification, the record to modify
|
||||
``context``
|
||||
context to use when creating the record
|
||||
``forcecreate``
|
||||
in update mode whether the record should be created if it doesn't exist
|
||||
|
||||
Requires an :term:`external id`, defaults to ``True``.
|
||||
|
||||
``field``
|
||||
----------
|
||||
|
||||
Each record can be composed of ``field`` tags, defining values to set when
|
||||
creating the record. A ``record`` with no ``field`` will use all default
|
||||
values (creation) or do nothing (update).
|
||||
|
||||
A ``field`` has a mandatory ``name`` attribute, the name of the field to set,
|
||||
and various methods to define the value itself:
|
||||
|
||||
Nothing
|
||||
if no value is provided for the field, an implicit ``False`` will be set
|
||||
on the field. Can be used to clear a field, or avoid using a default value
|
||||
for the field.
|
||||
``search``
|
||||
for :ref:`relational fields <reference/fields/relational>`, should be
|
||||
a :ref:`domain <reference/orm/domains>` on the field's model.
|
||||
|
||||
Will evaluate the domain, search the field's model using it and set the
|
||||
search's result as the field's value. Will only use the first result if
|
||||
the field is a :class:`~odoo.fields.Many2one`
|
||||
``ref``
|
||||
if a ``ref`` attribute is provided, its value must be a valid
|
||||
:term:`external id`, which will be looked up and set as the field's value.
|
||||
|
||||
Mostly for :class:`~odoo.fields.Many2one` and
|
||||
:class:`~odoo.fields.Reference` fields
|
||||
``type``
|
||||
if a ``type`` attribute is provided, it is used to interpret and convert
|
||||
the field's content. The field's content can be provided through an
|
||||
external file using the ``file`` attribute, or through the node's body.
|
||||
|
||||
Available types are:
|
||||
|
||||
``xml``, ``html``
|
||||
extracts the ``field``'s children as a single document, evaluates
|
||||
any :term:`external id` specified with the form ``%(external_id)s``.
|
||||
``%%`` can be used to output actual *%* signs.
|
||||
``file``
|
||||
ensures that the field content is a valid file path in the current
|
||||
model, saves the pair :samp:`{module},{path}` as the field value
|
||||
``char``
|
||||
sets the field content directly as the field's value without
|
||||
alterations
|
||||
``base64``
|
||||
base64_-encodes the field's content, useful combined with the ``file``
|
||||
*attribute* to load e.g. image data into attachments
|
||||
``int``
|
||||
converts the field's content to an integer and sets it as the field's
|
||||
value
|
||||
``float``
|
||||
converts the field's content to a float and sets it as the field's
|
||||
value
|
||||
``list``, ``tuple``
|
||||
should contain any number of ``value`` elements with the same
|
||||
properties as ``field``, each element resolves to an item of a
|
||||
generated tuple or list, and the generated collection is set as the
|
||||
field's value
|
||||
``eval``
|
||||
for cases where the previous methods are unsuitable, the ``eval``
|
||||
attributes simply evaluates whatever Python expression it is provided and
|
||||
sets the result as the field's value.
|
||||
|
||||
The evaluation context contains various modules (``time``, ``datetime``,
|
||||
``timedelta``, ``relativedelta``), a function to resolve :term:`external
|
||||
identifiers` (``ref``) and the model object for the current field if
|
||||
applicable (``obj``)
|
||||
|
||||
``delete``
|
||||
----------
|
||||
|
||||
The ``delete`` tag can remove any number of records previously defined. It
|
||||
has the following attributes:
|
||||
|
||||
``model`` (required)
|
||||
the model in which a specified record should be deleted
|
||||
``id``
|
||||
the :term:`external id` of a record to remove
|
||||
``search``
|
||||
a :ref:`domain <reference/orm/domains>` to find records of the model to
|
||||
remove
|
||||
|
||||
``id`` and ``search`` are exclusive
|
||||
|
||||
``function``
|
||||
------------
|
||||
|
||||
The ``function`` tag calls a method on a model, with provided parameters.
|
||||
It has two mandatory parameters ``model`` and ``name`` specifying respectively
|
||||
the model and the name of the method to call.
|
||||
|
||||
Parameters can be provided using ``eval`` (should evaluate to a sequence of
|
||||
parameters to call the method with) or ``value`` elements (see ``list``
|
||||
values).
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record name="partner_1" model="res.partner">
|
||||
<field name="name">Odude</field>
|
||||
</record>
|
||||
|
||||
<function model="res.partner" name="send_inscription_notice"
|
||||
eval="[[ref('partner_1'), ref('partner_2')]]"/>
|
||||
|
||||
<function model="res.users" name="send_vip_inscription_notice">
|
||||
<function eval="[[('vip','=',True)]]" model="res.partner" name="search"/>
|
||||
</function>
|
||||
</data>
|
||||
|
||||
<record id="model_form_view" model="ir.ui.view">
|
||||
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. ignored assert
|
||||
|
||||
.. _reference/data/shortcuts:
|
||||
|
||||
Shortcuts
|
||||
=========
|
||||
|
||||
Because some important structural models of Odoo are complex and involved,
|
||||
data files provide shorter alternatives to defining them using
|
||||
:ref:`record tags <reference/data/record>`:
|
||||
|
||||
``menuitem``
|
||||
------------
|
||||
|
||||
Defines an ``ir.ui.menu`` record with a number of defaults and fallbacks:
|
||||
|
||||
``parent``
|
||||
* If a ``parent`` attribute is set, it should be the :term:`external id`
|
||||
of an other menu item, used as the new item's parent
|
||||
* If no ``parent`` is provided, tries to interpret the ``name`` attribute
|
||||
as a ``/``-separated sequence of menu names and find a place in the menu
|
||||
hierarchy. In that interpretation, intermediate menus are automatically
|
||||
created
|
||||
* Otherwise the menu is defined as a "top-level" menu item (*not* a menu
|
||||
with no parent)
|
||||
``name``
|
||||
If no ``name`` attribute is specified, tries to get the menu name from
|
||||
a linked action if any. Otherwise uses the record's ``id``
|
||||
``groups``
|
||||
A ``groups`` attribute is interpreted as a comma-separated sequence of
|
||||
:term:`external identifiers` for ``res.groups`` models. If an
|
||||
:term:`external identifier` is prefixed with a minus (``-``), the group
|
||||
is *removed* from the menu's groups
|
||||
``action``
|
||||
if specified, the ``action`` attribute should be the :term:`external id`
|
||||
of an action to execute when the menu is open
|
||||
``id``
|
||||
the menu item's :term:`external id`
|
||||
|
||||
.. _reference/data/template:
|
||||
|
||||
``template``
|
||||
------------
|
||||
|
||||
Creates a :ref:`QWeb view <reference/views/qweb>` requiring only the ``arch``
|
||||
section of the view, and allowing a few *optional* attributes:
|
||||
|
||||
``id``
|
||||
the view's :term:`external identifier`
|
||||
``name``, ``inherit_id``, ``priority``
|
||||
same as the corresponding field on ``ir.ui.view`` (nb: ``inherit_id``
|
||||
should be an :term:`external identifier`)
|
||||
``primary``
|
||||
if set to ``True`` and combined with a ``inherit_id``, defines the view
|
||||
as a primary
|
||||
``groups``
|
||||
comma-separated list of group :term:`external identifiers`
|
||||
``page``
|
||||
if set to ``"True"``, the template is a website page (linkable to,
|
||||
deletable)
|
||||
``optional``
|
||||
``enabled`` or ``disabled``, whether the view can be disabled (in the
|
||||
website interface) and its default status. If unset, the view is always
|
||||
enabled.
|
||||
|
||||
``report``
|
||||
----------
|
||||
|
||||
Creates a :ref:`IrActionsReport <reference/actions/report>` record with a few default values.
|
||||
|
||||
Mostly just proxies attributes to the corresponding fields on
|
||||
``ir.actions.report``, but also automatically creates the item in the
|
||||
:guilabel:`More` menu of the report's ``model``.
|
||||
|
||||
.. note::
|
||||
|
||||
You might expect the ``name`` of the ``report`` tag to become the ``ir.actions.report`` name,
|
||||
but the value is used as ``report_name`` field value. To specify the ``name`` field in ``ir.actions.report``,
|
||||
you should use the ``string`` attribute of the ``report`` tag.
|
||||
|
||||
The detailed attributes and values supported can be found :ref:`here <reference/reports/report>`.
|
||||
|
||||
.. ignored url, act_window and ir_set
|
||||
|
||||
CSV data files
|
||||
==============
|
||||
|
||||
XML data files are flexible and self-descriptive, but very verbose when
|
||||
creating a number of simple records of the same model in bulk.
|
||||
|
||||
For this case, data files can also use csv_, this is often the case for
|
||||
:ref:`access rights <reference/security/acl>`:
|
||||
|
||||
* the file name is :file:`{model_name}.csv`
|
||||
* the first row lists the fields to write, with the special field ``id``
|
||||
for :term:`external identifiers` (used for creation or update)
|
||||
* each row thereafter creates a new record
|
||||
|
||||
Here's the first lines of the data file defining US states
|
||||
``res.country.state.csv``
|
||||
|
||||
.. literalinclude:: static/res.country.state.csv
|
||||
:language: text
|
||||
|
||||
rendered in a more readable format:
|
||||
|
||||
.. csv-table::
|
||||
:file: static/res.country.state.csv
|
||||
:header-rows: 1
|
||||
:class: table-striped table-hover table-sm
|
||||
|
||||
For each row (record):
|
||||
|
||||
* the first column is the :term:`external id` of the record to create or
|
||||
update
|
||||
* the second column is the :term:`external id` of the country object to link
|
||||
to (country objects must have been defined beforehand)
|
||||
* the third column is the ``name`` field for ``res.country.state``
|
||||
* the fourth column is the ``code`` field for ``res.country.state``
|
||||
|
||||
.. _base64: https://tools.ietf.org/html/rfc3548.html#section-3
|
||||
.. _csv: https://en.wikipedia.org/wiki/Comma-separated_values
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
.. _reference/controllers:
|
||||
|
||||
===============
|
||||
Web Controllers
|
||||
===============
|
||||
|
||||
Controllers
|
||||
===========
|
||||
|
||||
Controllers need to provide extensibility, much like
|
||||
:class:`~odoo.models.Model`, but can't use the same mechanism as the
|
||||
pre-requisites (a database with loaded modules) may not be available yet (e.g.
|
||||
no database created, or no database selected).
|
||||
|
||||
Controllers thus provide their own extension mechanism, separate from that of
|
||||
models:
|
||||
|
||||
Controllers are created by :ref:`inheriting <python:tut-inheritance>` from :class:`~odoo.http.Controller`.
|
||||
Routes are defined through methods decorated with :func:`~odoo.http.route`::
|
||||
|
||||
class MyController(odoo.http.Controller):
|
||||
@route('/some_url', auth='public')
|
||||
def handler(self):
|
||||
return stuff()
|
||||
|
||||
To *override* a controller, :ref:`inherit <python:tut-inheritance>` from its
|
||||
class and override relevant methods, re-exposing them if necessary::
|
||||
|
||||
class Extension(MyController):
|
||||
@route()
|
||||
def handler(self):
|
||||
do_before()
|
||||
return super(Extension, self).handler()
|
||||
|
||||
* decorating with :func:`~odoo.http.route` is necessary to keep the method
|
||||
(and route) visible: if the method is redefined without decorating, it
|
||||
will be "unpublished"
|
||||
* the decorators of all methods are combined, if the overriding method's
|
||||
decorator has no argument all previous ones will be kept, any provided
|
||||
argument will override previously defined ones e.g.::
|
||||
|
||||
class Restrict(MyController):
|
||||
@route(auth='user')
|
||||
def handler(self):
|
||||
return super(Restrict, self).handler()
|
||||
|
||||
will change ``/some_url`` from public authentication to user (requiring a
|
||||
log-in)
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
.. _reference/http/routing:
|
||||
|
||||
Routing
|
||||
-------
|
||||
|
||||
.. autofunction:: odoo.http.route
|
||||
|
||||
.. _reference/http/request:
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
The request object is automatically set on :data:`odoo.http.request` at
|
||||
the start of the request
|
||||
|
||||
.. autoclass:: odoo.http.WebRequest
|
||||
:members:
|
||||
:member-order: bysource
|
||||
.. autoclass:: odoo.http.HttpRequest
|
||||
:members:
|
||||
.. autoclass:: odoo.http.JsonRequest
|
||||
:members:
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
.. autoclass:: odoo.http.Response
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. maybe set this to document all the fine methods on Werkzeug's Response
|
||||
object? (it works)
|
||||
:inherited-members:
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,139 @@
|
||||
|
||||
==================
|
||||
Internet of Things
|
||||
==================
|
||||
|
||||
IoT Drivers allow any Odoo module to communicate in real-time with any device
|
||||
connected to the IoT Box. Communication with the IoT Box goes both ways, so the
|
||||
Odoo client can send commands to and receive information from any of the
|
||||
supported devices. To add support for a device, all we need is a `Driver`.
|
||||
|
||||
At each boot, the IoT Box will load all of the Drivers that can
|
||||
be located on the connected Odoo instance. Each module can contain a
|
||||
`drivers` directory, whose content will be copied to the IoT Box.
|
||||
|
||||
Detect Devices
|
||||
==============
|
||||
|
||||
The `addons/hw_drivers/controllers/driver.py` file contains a Manager that is
|
||||
in charge of the devices. The Manager maintains a list of connected devices
|
||||
and associates them with the right Driver.
|
||||
|
||||
Supported devices will appear both on the IoT Box Homepage that you can access
|
||||
through its IP address and in the IoT module of the connected Odoo instance.
|
||||
|
||||
Driver
|
||||
------
|
||||
|
||||
Once the Manager has retrieved the list of detected devices, it will loop
|
||||
through all of the Drivers that have the same connection type and test their
|
||||
respective `supported` method on all detected devices. If the supported method
|
||||
of a Driver returns `True`, an instance of this Driver will be created for the
|
||||
corresponding device.
|
||||
|
||||
Creating a new Driver requires:
|
||||
|
||||
- Extending `Driver`
|
||||
- Setting the `connection_type` class attribute.
|
||||
- Setting the `device_type`, `device_connection` and `device_name` attributes.
|
||||
- Defining the `supported` method
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.addons.hw_drivers.controllers.driver import Driver
|
||||
|
||||
class DriverName(Driver):
|
||||
connection_type = 'ConnectionType'
|
||||
|
||||
def __init__(self, device):
|
||||
super(NewDriver, self).__init__(device)
|
||||
self._device_type = 'DeviceType'
|
||||
self._device_connection = 'DeviceConnection'
|
||||
self._device_name = 'DeviceName'
|
||||
|
||||
@classmethod
|
||||
def supported(cls, device):
|
||||
...
|
||||
|
||||
Communicate With Devices
|
||||
========================
|
||||
|
||||
Once your new device is detected and appears in the IoT module, the next step
|
||||
is to communicate with it. Since the box only has a local IP address, it can
|
||||
only be reached from the same local network. Communication, therefore, needs to
|
||||
happen on the browser-side, in JavaScript.
|
||||
|
||||
The process depends on the direction of the communication:
|
||||
- From the browser to the box, through `Actions`_
|
||||
- From the box to the browser, through `Longpolling`_
|
||||
|
||||
Both channels are accessed from the same JS object, the `DeviceProxy`, which is
|
||||
instantiated using the IP of the IoT Box and the device identifier.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var DeviceProxy = require('iot.widgets').DeviceProxy;
|
||||
|
||||
var iot_device = new DeviceProxy({
|
||||
iot_ip: iot_ip,
|
||||
identifier: device_identifier
|
||||
});
|
||||
|
||||
Actions
|
||||
-------
|
||||
|
||||
Actions are used to tell a selected device to execute a specific action,
|
||||
such as taking a picture, printing a receipt, etc.
|
||||
|
||||
.. note::
|
||||
It must be noted that no “answer” will be sent by the box on this route,
|
||||
only the request status. The answer to the action, if any, has to be
|
||||
retrieved via the longpolling.
|
||||
|
||||
An action can be performed on the DeviceProxy Object.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
iot_device.action(data);
|
||||
|
||||
In your driver, define an `action` method that will be executed when called
|
||||
from an Odoo module. It takes the data given during the call as argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def action(self, data):
|
||||
...
|
||||
|
||||
Longpolling
|
||||
-----------
|
||||
|
||||
When any module in Odoo wants to read data from a specific device, it creates a
|
||||
listener identified by the IP/domain of the box and the device identifier and
|
||||
passes it a callback function to be called every time the device status
|
||||
changes. The callback is called with the new data as argument.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
iot_device.add_listener(this._onValueChange.bind(this));
|
||||
|
||||
_onValueChange: function (result) {
|
||||
...
|
||||
}
|
||||
|
||||
In the Driver, an event is released by calling the `device_changed` function
|
||||
from the `event_manager`. All callbacks set on the listener will then be called
|
||||
with `self.data` as argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.addons.hw_drivers.controllers.driver import event_manager
|
||||
|
||||
class DriverName(Driver):
|
||||
connection_type = 'ConnectionType'
|
||||
|
||||
def methodName(self):
|
||||
self.data = {
|
||||
'value': 0.5,
|
||||
...
|
||||
}
|
||||
event_manager.device_changed(self)
|
||||
@@ -0,0 +1,544 @@
|
||||
|
||||
.. _reference/jscs:
|
||||
|
||||
=====================
|
||||
Javascript Cheatsheet
|
||||
=====================
|
||||
|
||||
There are many ways to solve a problem in JavaScript, and in Odoo. However, the
|
||||
Odoo framework was designed to be extensible (this is a pretty big constraint),
|
||||
and some common problems have a nice standard solution. The standard solution
|
||||
has probably the advantage of being easy to understand for an odoo developers,
|
||||
and will probably keep working when Odoo is modified.
|
||||
|
||||
This document tries to explain the way one could solve some of these issues.
|
||||
Note that this is not a reference. This is just a random collection of recipes,
|
||||
or explanations on how to proceed in some cases.
|
||||
|
||||
|
||||
First of all, remember that the first rule of customizing odoo with JS is:
|
||||
*try to do it in python*. This may seem strange, but the python framework is
|
||||
quite extensible, and many behaviours can be done simply with a touch of xml or
|
||||
python. This has usually a lower cost of maintenance than working with JS:
|
||||
|
||||
- the JS framework tends to change more, so JS code needs to be more frequently
|
||||
updated
|
||||
- it is often more difficult to implement a customized behaviour if it needs to
|
||||
communicate with the server and properly integrate with the javascript framework.
|
||||
There are many small details taken care by the framework that customized code
|
||||
needs to replicate. For example, responsiveness, or updating the url, or
|
||||
displaying data without flickering.
|
||||
|
||||
|
||||
.. note:: This document does not really explain any concepts. This is more a
|
||||
cookbook. For more details, please consult the javascript reference
|
||||
page (see :doc:`javascript_reference`)
|
||||
|
||||
Creating a new field widget
|
||||
===========================
|
||||
|
||||
This is probably a really common usecase: we want to display some information in
|
||||
a form view in a really specific (maybe business dependent) way. For example,
|
||||
assume that we want to change the text color depending on some business condition.
|
||||
|
||||
This can be done in three steps: creating a new widget, registering it in the
|
||||
field registry, then adding the widget to the field in the form view
|
||||
|
||||
- creating a new widget:
|
||||
This can be done by extending a widget:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var FieldChar = require('web.basic_fields').FieldChar;
|
||||
|
||||
var CustomFieldChar = FieldChar.extend({
|
||||
_renderReadonly: function () {
|
||||
// implement some custom logic here
|
||||
},
|
||||
});
|
||||
|
||||
- registering it in the field registry:
|
||||
The web client needs to know the mapping between a widget name and its
|
||||
actual class. This is done by a registry:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
|
||||
fieldRegistry.add('my-custom-field', CustomFieldChar);
|
||||
|
||||
- adding the widget in the form view
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="my-custom-field"/>
|
||||
|
||||
Note that only the form, list and kanban views use this field widgets registry.
|
||||
These views are tightly integrated, because the list and kanban views can
|
||||
appear inside a form view).
|
||||
|
||||
Modifying an existing field widget
|
||||
==================================
|
||||
|
||||
Another use case is that we want to modify an existing field widget. For
|
||||
example, the voip addon in odoo need to modify the FieldPhone widget to add the
|
||||
possibility to easily call the given number on voip. This is done by *including*
|
||||
the FieldPhone widget, so there is no need to change any existing form view.
|
||||
|
||||
Field Widgets (instances of (subclass of) AbstractField) are like every other
|
||||
widgets, so they can be monkey patched. This looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var Phone = basic_fields.FieldPhone;
|
||||
|
||||
Phone.include({
|
||||
events: _.extend({}, Phone.prototype.events, {
|
||||
'click': '_onClick',
|
||||
}),
|
||||
|
||||
_onClick: function (e) {
|
||||
if (this.mode === 'readonly') {
|
||||
e.preventDefault();
|
||||
var phoneNumber = this.value;
|
||||
// call the number on voip...
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Note that there is no need to add the widget to the registry, since it is already
|
||||
registered.
|
||||
|
||||
Modifying a main widget from the interface
|
||||
==========================================
|
||||
|
||||
Another common usecase is the need to customize some elements from the user
|
||||
interface. For example, adding a message in the home menu. The usual process
|
||||
in this case is again to *include* the widget. This is the only way to do it,
|
||||
since there are no registries for those widgets.
|
||||
|
||||
This is usually done with code looking like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var HomeMenu = require('web_enterprise.HomeMenu');
|
||||
|
||||
HomeMenu.include({
|
||||
render: function () {
|
||||
this._super();
|
||||
// do something else here...
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
Creating a new view (from scratch)
|
||||
==================================
|
||||
|
||||
Creating a new view is a more advanced topic. This cheatsheet will only
|
||||
highlight the steps that will probably need to be done (in no particular order):
|
||||
|
||||
- adding a new view type to the field ``type`` of ``ir.ui.view``::
|
||||
|
||||
class View(models.Model):
|
||||
_inherit = 'ir.ui.view'
|
||||
|
||||
type = fields.Selection(selection_add=[('map', "Map")])
|
||||
|
||||
- adding the new view type to the field ``view_mode`` of ``ir.actions.act_window.view``::
|
||||
|
||||
class ActWindowView(models.Model):
|
||||
_inherit = 'ir.actions.act_window.view'
|
||||
|
||||
view_mode = fields.Selection(selection_add=[('map', "Map")])
|
||||
|
||||
|
||||
- creating the four main pieces which makes a view (in JavaScript):
|
||||
we need a view (a subclass of ``AbstractView``, this is the factory), a
|
||||
renderer (from ``AbstractRenderer``), a controller (from ``AbstractController``)
|
||||
and a model (from ``AbstractModel``). I suggest starting by simply
|
||||
extending the superclasses:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var AbstractModel = require('web.AbstractModel');
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var AbstractView = require('web.AbstractView');
|
||||
|
||||
var MapController = AbstractController.extend({});
|
||||
var MapRenderer = AbstractRenderer.extend({});
|
||||
var MapModel = AbstractModel.extend({});
|
||||
|
||||
var MapView = AbstractView.extend({
|
||||
config: {
|
||||
Model: MapModel,
|
||||
Controller: MapController,
|
||||
Renderer: MapRenderer,
|
||||
},
|
||||
});
|
||||
|
||||
- adding the view to the registry:
|
||||
As usual, the mapping between a view type and the actual class needs to be
|
||||
updated:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
viewRegistry.add('map', MapView);
|
||||
|
||||
- implementing the four main classes:
|
||||
The ``View`` class needs to parse the ``arch`` field and setup the other
|
||||
three classes. The ``Renderer`` is in charge of representing the data in
|
||||
the user interface, the ``Model`` is supposed to talk to the server, to
|
||||
load data and process it. And the ``Controller`` is there to coordinate,
|
||||
to talk to the web client, ...
|
||||
|
||||
- creating some views in the database:
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="customer_map_view" model="ir.ui.view">
|
||||
<field name="name">customer.map.view</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<map latitude="partner_latitude" longitude="partner_longitude">
|
||||
<field name="name"/>
|
||||
</map>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
Customizing an existing view
|
||||
============================
|
||||
|
||||
Assume we need to create a custom version of a generic view. For example, a
|
||||
kanban view with some extra *ribbon-like* widget on top (to display some
|
||||
specific custom information). In that case, this can be done with 3 steps:
|
||||
extend the kanban view (which also probably mean extending controllers/renderers
|
||||
and/or models), then registering the view in the view registry, and finally,
|
||||
using the view in the kanban arch (a specific example is the helpdesk dashboard).
|
||||
|
||||
- extending a view:
|
||||
Here is what it could look like:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var HelpdeskDashboardRenderer = KanbanRenderer.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardModel = KanbanModel.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardController = KanbanController.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Model: HelpdeskDashboardModel,
|
||||
Renderer: HelpdeskDashboardRenderer,
|
||||
Controller: HelpdeskDashboardController,
|
||||
}),
|
||||
});
|
||||
|
||||
- adding it to the view registry:
|
||||
as usual, we need to inform the web client of the mapping between the name
|
||||
of the views and the actual class.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
|
||||
|
||||
- using it in an actual view:
|
||||
we now need to inform the web client that a specific ``ir.ui.view`` needs to
|
||||
use our new class. Note that this is a web client specific concern. From
|
||||
the point of view of the server, we still have a kanban view. The proper
|
||||
way to do this is by using a special attribute ``js_class`` (which will be
|
||||
renamed someday into ``widget``, because this is really not a good name) on
|
||||
the root node of the arch:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="helpdesk_team_view_kanban" model="ir.ui.view" >
|
||||
...
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="helpdesk_dashboard">
|
||||
...
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
.. note::
|
||||
|
||||
Note: you can change the way the view interprets the arch structure. However,
|
||||
from the server point of view, this is still a view of the same base type,
|
||||
subjected to the same rules (rng validation, for example). So, your views still
|
||||
need to have a valid arch field.
|
||||
|
||||
Promises and asynchronous code
|
||||
===============================
|
||||
|
||||
For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md
|
||||
|
||||
Creating new Promises
|
||||
-----------------------
|
||||
- turn a constant into a promise
|
||||
There are 2 static functions on Promise that create a resolved or rejected promise based on a constant:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var p = Promise.resolve({blabla: '1'}); // creates a resolved promise
|
||||
p.then(function (result) {
|
||||
console.log(result); // --> {blabla: '1'};
|
||||
});
|
||||
|
||||
|
||||
var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise
|
||||
p2.catch(function (reason) {
|
||||
console.log(reason); // --> {error: 'error message');
|
||||
});
|
||||
|
||||
|
||||
.. note:: Note that even if the promises are created already resolved or rejected, the `then` or `catch` handlers will still be called asynchronously.
|
||||
|
||||
|
||||
- based on an already asynchronous code
|
||||
Suppose that in a function you must do a rpc, and when it is completed set the result on this.
|
||||
The `this._rpc` is a function that returns a `Promise`.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function callRpc() {
|
||||
var self = this;
|
||||
return this._rpc(...).then(function (result) {
|
||||
self.myValueFromRpc = result;
|
||||
});
|
||||
}
|
||||
|
||||
- for callback based function
|
||||
Suppose that you were using a function `this.close` that takes as parameter a callback that is called when the closing is finished.
|
||||
Now suppose that you are doing that in a method that must send a promise that is resolved when the closing is finished.
|
||||
|
||||
.. code-block:: javascript
|
||||
:linenos:
|
||||
|
||||
function waitForClose() {
|
||||
var self = this;
|
||||
return new Promise (function(resolve, reject) {
|
||||
self.close(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
* line 2: we save the `this` into a variable so that in an inner function, we can access the scope of our component
|
||||
* line 3: we create and return a new promise. The constructor of a promise takes a function as parameter. This function itself has 2 parameters that we called here `resolve` and `reject`
|
||||
- `resolve` is a function that, when called, puts the promise in the resolved state.
|
||||
- `reject` is a function that, when called, puts the promise in the rejected state. We do not use reject here and it can be omitted.
|
||||
* line 4: we are calling the function close on our object. It takes a function as parameter (the callback) and it happens that resolve is already a function, so we can pass it directly. To be clearer, we could have written:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
return new Promise (function (resolve) {
|
||||
self.close(function () {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
- creating a promise generator (calling one promise after the other *in sequence* and waiting for the last one)
|
||||
Suppose that you need to loop over an array, do an operation *in sequence* and resolve a promise when the last operation is done.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function doStuffOnArray(arr) {
|
||||
var done = Promise.resolve();
|
||||
arr.forEach(function (item) {
|
||||
done = done.then(function () {
|
||||
return item.doSomethingAsynchronous();
|
||||
});
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
This way, the promise you return is effectively the last promise.
|
||||
- creating a promise, then resolving it outside the scope of its definition (anti-pattern)
|
||||
.. note:: we do not recommend using this, but sometimes it is useful. Think carefully for alternatives first...
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
...
|
||||
var resolver, rejecter;
|
||||
var prom = new Promise(function (resolve, reject){
|
||||
resolver = resolve;
|
||||
rejecter = reject;
|
||||
});
|
||||
...
|
||||
|
||||
resolver("done"); // will resolve the promise prom with the result "done"
|
||||
rejecter("error"); // will reject the promise prom with the reason "error"
|
||||
|
||||
Waiting for Promises
|
||||
--------------------
|
||||
- waiting for a number of Promises
|
||||
if you have multiple promises that all need to be waited, you can convert them into a single promise that will be resolved when all the promises are resolved using Promise.all(arrayOfPromises).
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var prom1 = doSomethingThatReturnsAPromise();
|
||||
var prom2 = Promise.resolve(true);
|
||||
var constant = true;
|
||||
|
||||
var all = Promise.all([prom1, prom2, constant]); // all is a promise
|
||||
// results is an array, the individual results correspond to the index of their
|
||||
// promise as called in Promise.all()
|
||||
all.then(function (results) {
|
||||
var prom1Result = results[0];
|
||||
var prom2Result = results[1];
|
||||
var constantResult = results[2];
|
||||
});
|
||||
return all;
|
||||
|
||||
|
||||
- waiting for a part of a promise chain, but not another part
|
||||
If you have an asynchronous process that you want to wait to do something, but you also want to return to the caller before that something is done.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function returnAsSoonAsAsyncProcessIsDone() {
|
||||
var prom = AsyncProcess();
|
||||
prom.then(function (resultOfAsyncProcess) {
|
||||
return doSomething();
|
||||
});
|
||||
/* returns prom which will only wait for AsyncProcess(),
|
||||
and when it will be resolved, the result will be the one of AsyncProcess */
|
||||
return prom;
|
||||
}
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
|
||||
- in general in promises
|
||||
The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors.
|
||||
When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function a() {
|
||||
x.y(); // <-- this is an error: x is undefined
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
function b() {
|
||||
return Promise.reject(2);
|
||||
}
|
||||
|
||||
a().catch(console.log); // will log the error in a
|
||||
a().then(b).catch(console.log); // will log the error in a, the then is not executed
|
||||
b().catch(console.log); // will log the rejected reason of b (2)
|
||||
Promise.resolve(1)
|
||||
.then(b) // the then is executed, it executes b
|
||||
.then(...) // this then is not executed
|
||||
.catch(console.log); // will log the rejected reason of b (2)
|
||||
|
||||
|
||||
|
||||
- in Odoo specifically
|
||||
In Odoo, it happens that we use promise rejection for control flow, like in mutexes and other concurrency primitives defined in module `web.concurrency`
|
||||
We also want to execute the catch for *business* reasons, but not when there is a coding error in the definition of the promise or of the handlers.
|
||||
For this, we have introduced the concept of `guardedCatch`. It is called like `catch` but not when the rejected reason is an error
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function blabla() {
|
||||
if (someCondition) {
|
||||
return Promise.reject("someCondition is truthy");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
var promise = blabla();
|
||||
promise.then(function (result) { console.log("everything went fine"); })
|
||||
// this will be called if blabla returns a rejected promise, but not if it has an error
|
||||
promise.guardedCatch(function (reason) { console.log(reason); });
|
||||
|
||||
// ...
|
||||
|
||||
var anotherPromise =
|
||||
blabla().then(function () { console.log("everything went fine"); })
|
||||
// this will be called if blabla returns a rejected promise,
|
||||
// but not if it has an error
|
||||
.guardedCatch(console.log);
|
||||
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var promiseWithError = Promise.resolve().then(function () {
|
||||
x.y(); // <-- this is an error: x is undefined
|
||||
});
|
||||
promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called
|
||||
promiseWithError.catch(function (reason) {console.log(reason);}); // will be called
|
||||
|
||||
|
||||
|
||||
Testing asynchronous code
|
||||
--------------------------
|
||||
|
||||
- using promises in tests
|
||||
In the tests code, we support the latest version of Javascript, including primitives like `async` and `await`. This makes using and waiting for promises very easy.
|
||||
Most helper methods also return a promise (either by being marked `async` or by returning a promise directly.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
QUnit.test("My test", async function (assert) {
|
||||
// making the function async has 2 advantages:
|
||||
// 1) it always returns a promise so you don't need to define `var done = assert.async()`
|
||||
// 2) it allows you to use the `await`
|
||||
assert.expect(1);
|
||||
|
||||
var form = await testUtils.createView({ ... });
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.form.click('jquery selector');
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("My test - no async - no done", function (assert) {
|
||||
// this function is not async, but it returns a promise.
|
||||
// QUnit will wait for for this promise to be resolved.
|
||||
assert.expect(1);
|
||||
|
||||
return testUtils.createView({ ... }).then(function (form) {
|
||||
return testUtils.form.clickEdit(form).then(function () {
|
||||
return testUtils.form.click('jquery selector').then(function () {
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
QUnit.test("My test - no async", function (assert) {
|
||||
// this function is not async and does not return a promise.
|
||||
// we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback
|
||||
assert.expect(1);
|
||||
var done = assert.async();
|
||||
|
||||
testUtils.createView({ ... }).then(function (form) {
|
||||
testUtils.form.clickEdit(form).then(function () {
|
||||
testUtils.form.click('jquery selector').then(function () {
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.
|
||||
@@ -0,0 +1,211 @@
|
||||
|
||||
.. _reference/mobile:
|
||||
|
||||
==================
|
||||
Mobile JavaScript
|
||||
==================
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
In Odoo 10.0 we released a mobile app which allows you to access all **Odoo apps**
|
||||
(even your customized modules).
|
||||
|
||||
The application is a combination of **Odoo Web** and **Native Mobile
|
||||
components**. In other words it is a Odoo Web instance loaded inside a native, mobile, WebView container.
|
||||
|
||||
This page documents how you can access mobile native components like Camera,
|
||||
Vibration, Notification and Toast through Odoo Web (via JavaScript). For this, you
|
||||
do not need to be a mobile developer, if you know Odoo JavaScript API you can
|
||||
access all available mobile features.
|
||||
|
||||
.. warning:: These features work with **Odoo Enterprise 10.0+** only
|
||||
|
||||
How does it work?
|
||||
=================
|
||||
|
||||
Internal workings of the mobile application:
|
||||
|
||||
.. image:: images/mobile_working.jpg
|
||||
|
||||
Of course, it is a web page that loads on a Mobile Native Web container. But it
|
||||
is integrated in such a way that you can access native resources from your web
|
||||
JavaScript.
|
||||
|
||||
WebPages (Odoo Web) is on the top of each layer, where the second layer is a Bridge
|
||||
between Odoo Web (JS) and the native mobile components.
|
||||
|
||||
When any call from JavaScript is triggered it passes through Bridge and Bridge
|
||||
passes it to the native invoker to perform that action.
|
||||
|
||||
When the native component has done its work, it is passed to the Bridge again and
|
||||
you get the output in JavaScript.
|
||||
|
||||
Process time taken by the Native component depends on what you are requesting
|
||||
from the Native resources. For example the Camera or GPS Location.
|
||||
|
||||
How to use it?
|
||||
==============
|
||||
|
||||
Just like the Odoo Web Framework, the Mobile API can be used anywhere by getting the object from
|
||||
**web_mobile.rpc**
|
||||
|
||||
.. image:: images/odoo_mobile_api.png
|
||||
|
||||
The mobile RPC object provides a list of methods that are available (this only works with the mobile
|
||||
app).
|
||||
|
||||
Check if the method is available and then execute it.
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. note:: Each of the methods returns a JQuery Deffered object which returns
|
||||
a data JSON dictionary
|
||||
|
||||
Show Toast in device
|
||||
.....................
|
||||
|
||||
.. js:function:: showToast
|
||||
|
||||
:param object args: **message** text to display
|
||||
|
||||
A toast provides simple feedback about an operation in a small popup. It only
|
||||
fills the amount of space required for the message and the current activity
|
||||
remains visible and interactive.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
mobile.methods.showToast({'message': 'Message sent'});
|
||||
|
||||
.. image:: images/toast.png
|
||||
|
||||
|
||||
Vibrating device
|
||||
................
|
||||
|
||||
|
||||
.. js:function:: vibrate
|
||||
|
||||
:param object args: Vibrates constantly for the specified period of time
|
||||
(in milliseconds).
|
||||
|
||||
Vibrate mobile device with given duration.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
mobile.methods.vibrate({'duration': 100});
|
||||
|
||||
Show snackbar with action
|
||||
.........................
|
||||
|
||||
.. js:function:: showSnackBar
|
||||
|
||||
:param object args: (*required*) **Message** to show in snackbar and action **button label** in Snackbar (optional)
|
||||
:returns: ``True`` if the user clicks on the Action button, ``False`` if SnackBar auto dismissed after some time.
|
||||
|
||||
Snackbars provide lightweight feedback about an operation. They show a brief
|
||||
message at the bottom of the screen on mobile or in the lower left corner on larger devices.
|
||||
Snackbars appear above all the other elements on the screen and only one can be
|
||||
displayed at a time.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
mobile.methods.showSnackBar({'message': 'Message is deleted', 'btn_text': 'Undo'}).then(function(result){
|
||||
if(result){
|
||||
// Do undo operation
|
||||
}else{
|
||||
// Snack Bar dismissed
|
||||
}
|
||||
});
|
||||
|
||||
.. image:: images/snackbar.png
|
||||
|
||||
Showing notification
|
||||
.....................
|
||||
|
||||
.. js:function:: showNotification
|
||||
|
||||
:param object args: **title** (first row) of the notification, **message** (second row) of the notification, in a standard notification.
|
||||
|
||||
A notification is a message you can display to the user outside of your
|
||||
application's normal UI. When you tell the system to issue a notification, it
|
||||
first appears as an icon in the notification area. To see the details of the
|
||||
notification, the user opens the notification drawer. Both the notification
|
||||
area and the notification drawer are system-controlled areas that the user can
|
||||
view at any time.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
mobile.showNotification({'title': 'Simple Notification', 'message': 'This is a test for a simple notification'})
|
||||
|
||||
.. image:: images/mobile_notification.png
|
||||
|
||||
|
||||
Create contact in device
|
||||
.........................
|
||||
|
||||
.. js:function:: addContact
|
||||
|
||||
:param object args: Dictionary with contact details. Possible keys (name, mobile, phone, fax, email, website, street, street2, country_id, state_id, city, zip, parent_id, function and image)
|
||||
|
||||
Create a new device contact with the given contact details.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var contact = {
|
||||
'name': 'Michel Fletcher',
|
||||
'mobile': '9999999999',
|
||||
'phone': '7954856587',
|
||||
'fax': '765898745',
|
||||
'email': 'michel.fletcher@agrolait.example.com',
|
||||
'website': 'http://www.agrolait.com',
|
||||
'street': '69 rue de Namur',
|
||||
'street2': false,
|
||||
'country_id': [21, 'Belgium'],
|
||||
'state_id': false,
|
||||
'city': 'Wavre',
|
||||
'zip': '1300',
|
||||
'parent_id': [8, 'Agrolait'],
|
||||
'function': 'Analyst',
|
||||
'image': '<<BASE 64 Image Data>>'
|
||||
}
|
||||
|
||||
mobile.methods.addContact(contact);
|
||||
|
||||
.. image:: images/mobile_contact_create.png
|
||||
|
||||
Scanning barcodes
|
||||
..................
|
||||
|
||||
.. js:function:: scanBarcode
|
||||
|
||||
:returns: Scanned ``code`` from any barcode
|
||||
|
||||
The barcode API detects barcodes in real-time, on the device, in any orientation.
|
||||
|
||||
The barcode API can read the following barcode formats:
|
||||
|
||||
* 1D barcodes: EAN-13, EAN-8, UPC-A, UPC-E, Code-39, Code-93, Code-128, ITF, Codabar
|
||||
* 2D barcodes: QR Code, Data Matrix, PDF-417, AZTEC
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
mobile.methods.scanBarcode().then(function(code){
|
||||
if(code){
|
||||
// Perform operation with the scanned code
|
||||
}
|
||||
});
|
||||
|
||||
Switching account in device
|
||||
...........................
|
||||
|
||||
.. js:function:: switchAccount
|
||||
|
||||
Use switchAccount to switch from one account to another on the device.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
mobile.methods.switchAccount();
|
||||
|
||||
.. image:: images/mobile_switch_account.png
|
||||
@@ -0,0 +1,147 @@
|
||||
|
||||
================
|
||||
Module Manifests
|
||||
================
|
||||
|
||||
|
||||
|
||||
.. _reference/module/manifest:
|
||||
|
||||
Manifest
|
||||
========
|
||||
|
||||
The manifest file serves to declare a python package as an Odoo module
|
||||
and to specify module metadata.
|
||||
|
||||
It is a file called ``__manifest__.py`` and contains a single Python
|
||||
dictionary, where each key specifies module metadatum.
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'name': "A Module",
|
||||
'version': '1.0',
|
||||
'depends': ['base'],
|
||||
'author': "Author Name",
|
||||
'category': 'Category',
|
||||
'description': """
|
||||
Description text
|
||||
""",
|
||||
# data files always loaded at installation
|
||||
'data': [
|
||||
'views/mymodule_view.xml',
|
||||
],
|
||||
# data files containing optionally loaded demonstration data
|
||||
'demo': [
|
||||
'demo/demo_data.xml',
|
||||
],
|
||||
}
|
||||
|
||||
Available manifest fields are:
|
||||
|
||||
``name`` (``str``, required)
|
||||
the human-readable name of the module
|
||||
``version`` (``str``)
|
||||
this module's version, should follow `semantic versioning`_ rules
|
||||
``description`` (``str``)
|
||||
extended description for the module, in reStructuredText
|
||||
``author`` (``str``)
|
||||
name of the module author
|
||||
``website`` (``str``)
|
||||
website URL for the module author
|
||||
``license`` (``str``, defaults: ``LGPL-3``)
|
||||
distribution license for the module.
|
||||
Possible values:
|
||||
|
||||
* `GPL-2`
|
||||
* `GPL-2 or any later version`
|
||||
* `GPL-3`
|
||||
* `GPL-3 or any later version`
|
||||
* `AGPL-3`
|
||||
* `LGPL-3`
|
||||
* `Other OSI approved licence`
|
||||
* `OEEL-1` (Odoo Enterprise Edition License v1.0)
|
||||
* `OPL-1` (Odoo Proprietary License v1.0)
|
||||
* `Other proprietary`
|
||||
|
||||
``category`` (``str``, default: ``Uncategorized``)
|
||||
classification category within Odoo, rough business domain for the module.
|
||||
|
||||
Although using `existing categories`_ is recommended, the field is
|
||||
freeform and unknown categories are created on-the-fly. Category
|
||||
hierarchies can be created using the separator ``/`` e.g. ``Foo / Bar``
|
||||
will create a category ``Foo``, a category ``Bar`` as child category of
|
||||
``Foo``, and will set ``Bar`` as the module's category.
|
||||
``depends`` (``list(str)``)
|
||||
Odoo modules which must be loaded before this one, either because this
|
||||
module uses features they create or because it alters resources they
|
||||
define.
|
||||
|
||||
When a module is installed, all of its dependencies are installed before
|
||||
it. Likewise dependencies are loaded before a module is loaded.
|
||||
``data`` (``list(str)``)
|
||||
List of data files which must always be installed or updated with the
|
||||
module. A list of paths from the module root directory
|
||||
``demo`` (``list(str)``)
|
||||
List of data files which are only installed or updated in *demonstration
|
||||
mode*
|
||||
``auto_install`` (``bool``, default: ``False``)
|
||||
If ``True``, this module will automatically be installed if all of its
|
||||
dependencies are installed.
|
||||
|
||||
It is generally used for "link modules" implementing synergic integration
|
||||
between two otherwise independent modules.
|
||||
|
||||
For instance ``sale_crm`` depends on both ``sale`` and ``crm`` and is set
|
||||
to ``auto_install``. When both ``sale`` and ``crm`` are installed, it
|
||||
automatically adds CRM campaigns tracking to sale orders without either
|
||||
``sale`` or ``crm`` being aware of one another
|
||||
``external_dependencies`` (``dict(key=list(str))``)
|
||||
A dictionary containing python and/or binary dependencies.
|
||||
|
||||
For python dependencies, the ``python`` key must be defined for this
|
||||
dictionary and a list of python modules to be imported should be assigned
|
||||
to it.
|
||||
|
||||
For binary dependencies, the ``bin`` key must be defined for this
|
||||
dictionary and a list of binary executable names should be assigned to it.
|
||||
|
||||
The module won't be installed if either the python module is not installed
|
||||
in the host machine or the binary executable is not found within the
|
||||
host machine's PATH environment variable.
|
||||
``application`` (``bool``, default: ``False``)
|
||||
Whether the module should be considered as a fully-fledged application
|
||||
(``True``) or is just a technical module (``False``) that provides some
|
||||
extra functionality to an existing application module.
|
||||
``css`` (``list(str)``)
|
||||
Specify css files with custom rules to be imported, these files should be
|
||||
located in ``static/src/css`` inside the module.
|
||||
``images`` (``list(str)``)
|
||||
Specify image files to be used by the module.
|
||||
``installable`` (``bool`` default: ``True``)
|
||||
Whether a user should be able to install the module from the Web UI or not.
|
||||
``maintainer`` (``str``)
|
||||
Person or entity in charge of the maintenance of this module, by default
|
||||
it is assumed that the author is the maintainer.
|
||||
``{pre_init, post_init, uninstall}_hook`` (``str``)
|
||||
Hooks for module installation/uninstallation, their value should be a
|
||||
string representing the name of a function defined inside the module's
|
||||
``__init__.py``.
|
||||
|
||||
``pre_init_hook`` takes a cursor as its only argument, this function is
|
||||
executed prior to the module's installation.
|
||||
|
||||
``post_init_hook`` takes a cursor and a registry as its arguments, this
|
||||
function is executed right after the module's installation.
|
||||
|
||||
``uninstall_hook`` takes a cursor and a registry as its arguments, this
|
||||
function is executed after the module's uninstallation.
|
||||
|
||||
These hooks should only be used when setup/cleanup required for this module
|
||||
is either extremely difficult or impossible through the api.
|
||||
``active`` (``bool``)
|
||||
This indicates whether this module must install automatically or not.
|
||||
|
||||
.. _semantic versioning: https://semver.org
|
||||
.. _existing categories:
|
||||
https://github.com/odoo/odoo/blob/13.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
@@ -0,0 +1,685 @@
|
||||
|
||||
.. highlight:: xml
|
||||
|
||||
.. _reference/qweb:
|
||||
|
||||
====
|
||||
QWeb
|
||||
====
|
||||
|
||||
QWeb is the primary templating_ engine used by Odoo\ [#othertemplates]_. It
|
||||
is an XML templating engine\ [#genshif]_ and used mostly to generate HTML_
|
||||
fragments and pages.
|
||||
|
||||
Template directives are specified as XML attributes prefixed with ``t-``,
|
||||
for instance ``t-if`` for :ref:`reference/qweb/conditionals`, with elements
|
||||
and other attributes being rendered directly.
|
||||
|
||||
To avoid element rendering, a placeholder element ``<t>`` is also available,
|
||||
which executes its directive but doesn't generate any output in and of
|
||||
itself::
|
||||
|
||||
<t t-if="condition">
|
||||
<p>Test</p>
|
||||
</t>
|
||||
|
||||
will result in::
|
||||
|
||||
<p>Test</p>
|
||||
|
||||
if ``condition`` is true, but::
|
||||
|
||||
<div t-if="condition">
|
||||
<p>Test</p>
|
||||
</div>
|
||||
|
||||
will result in::
|
||||
|
||||
<div>
|
||||
<p>Test</p>
|
||||
</div>
|
||||
|
||||
.. _reference/qweb/output:
|
||||
|
||||
Data output
|
||||
===========
|
||||
|
||||
QWeb has a primary output directive which automatically HTML-escape its
|
||||
content limiting XSS_ risks when displaying user-provided content: ``esc``.
|
||||
|
||||
``esc`` takes an expression, evaluates it and prints the content::
|
||||
|
||||
<p><t t-esc="value"/></p>
|
||||
|
||||
rendered with the value ``value`` set to ``42`` yields::
|
||||
|
||||
<p>42</p>
|
||||
|
||||
There is one other output directive ``raw`` which behaves the same as
|
||||
respectively ``esc`` but *does not HTML-escape its output*. It can be useful
|
||||
to display separately constructed markup (e.g. from functions) or already
|
||||
sanitized user-provided markup.
|
||||
|
||||
.. _reference/qweb/conditionals:
|
||||
|
||||
Conditionals
|
||||
============
|
||||
|
||||
QWeb has a conditional directive ``if``, which evaluates an expression given
|
||||
as attribute value::
|
||||
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>ok</p>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
The element is rendered if the condition is true::
|
||||
|
||||
<div>
|
||||
<p>ok</p>
|
||||
</div>
|
||||
|
||||
but if the condition is false it is removed from the result::
|
||||
|
||||
<div>
|
||||
</div>
|
||||
|
||||
The conditional rendering applies to the bearer of the directive, which does
|
||||
not have to be ``<t>``::
|
||||
|
||||
<div>
|
||||
<p t-if="condition">ok</p>
|
||||
</div>
|
||||
|
||||
will give the same results as the previous example.
|
||||
|
||||
Extra conditional branching directives ``t-elif`` and ``t-else`` are also
|
||||
available::
|
||||
|
||||
<div>
|
||||
<p t-if="user.birthday == today()">Happy birthday!</p>
|
||||
<p t-elif="user.login == 'root'">Welcome master!</p>
|
||||
<p t-else="">Welcome!</p>
|
||||
</div>
|
||||
|
||||
|
||||
.. _reference/qweb/loops:
|
||||
|
||||
Loops
|
||||
=====
|
||||
|
||||
QWeb has an iteration directive ``foreach`` which take an expression returning
|
||||
the collection to iterate on, and a second parameter ``t-as`` providing the
|
||||
name to use for the "current item" of the iteration::
|
||||
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
|
||||
Like conditions, ``foreach`` applies to the element bearing the directive's
|
||||
attribute, and
|
||||
|
||||
::
|
||||
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
``foreach`` can iterate on an array (the current item will be the current
|
||||
value) or a mapping (the current item will be the current key). Iterating on an
|
||||
integer (equivalent to iterating on an array between 0 inclusive and the
|
||||
provided integer exclusive) is still supported but deprecated.
|
||||
|
||||
In addition to the name passed via ``t-as``, ``foreach`` provides a few other
|
||||
variables for various data points:
|
||||
|
||||
.. warning:: ``$as`` will be replaced by the name passed to ``t-as``
|
||||
|
||||
:samp:`{$as}_all` (deprecated)
|
||||
the object being iterated over
|
||||
|
||||
.. note:: This variable is only available on JavaScript QWeb, not Python.
|
||||
|
||||
:samp:`{$as}_value`
|
||||
the current iteration value, identical to ``$as`` for lists and integers,
|
||||
but for mappings it provides the value (where ``$as`` provides the key)
|
||||
:samp:`{$as}_index`
|
||||
the current iteration index (the first item of the iteration has index 0)
|
||||
:samp:`{$as}_size`
|
||||
the size of the collection if it is available
|
||||
:samp:`{$as}_first`
|
||||
whether the current item is the first of the iteration (equivalent to
|
||||
:samp:`{$as}_index == 0`)
|
||||
:samp:`{$as}_last`
|
||||
whether the current item is the last of the iteration (equivalent to
|
||||
:samp:`{$as}_index + 1 == {$as}_size`), requires the iteratee's size be
|
||||
available
|
||||
:samp:`{$as}_parity` (deprecated)
|
||||
either ``"even"`` or ``"odd"``, the parity of the current iteration round
|
||||
:samp:`{$as}_even` (deprecated)
|
||||
a boolean flag indicating that the current iteration round is on an even
|
||||
index
|
||||
:samp:`{$as}_odd` (deprecated)
|
||||
a boolean flag indicating that the current iteration round is on an odd
|
||||
index
|
||||
|
||||
These extra variables provided and all new variables created into the
|
||||
``foreach`` are only available in the scope of the``foreach``. If the
|
||||
variable exists outside the context of the ``foreach``, the value is copied
|
||||
at the end of the foreach into the global context.
|
||||
|
||||
::
|
||||
|
||||
<t t-set="existing_variable" t-value="False"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-set="existing_variable" t-value="True"/>
|
||||
<t t-set="new_variable" t-value="True"/>
|
||||
<!-- existing_variable and new_variable now True -->
|
||||
</p>
|
||||
|
||||
<!-- existing_variable always True -->
|
||||
<!-- new_variable undefined -->
|
||||
|
||||
.. _reference/qweb/attributes:
|
||||
|
||||
attributes
|
||||
==========
|
||||
|
||||
QWeb can compute attributes on-the-fly and set the result of the computation
|
||||
on the output node. This is done via the ``t-att`` (attribute) directive which
|
||||
exists in 3 different forms:
|
||||
|
||||
:samp:`t-att-{$name}`
|
||||
an attribute called ``$name`` is created, the attribute value is evaluated
|
||||
and the result is set as the attribute's value::
|
||||
|
||||
<div t-att-a="42"/>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<div a="42"></div>
|
||||
:samp:`t-attf-{$name}`
|
||||
same as previous, but the parameter is a :term:`format string`
|
||||
instead of just an expression, often useful to mix literal and non-literal
|
||||
string (e.g. classes)::
|
||||
|
||||
<t t-foreach="[1, 2, 3]" t-as="item">
|
||||
<li t-attf-class="row {{ (item_index % 2 === 0) ? 'even' : 'odd' }}">
|
||||
<t t-esc="item"/>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<li class="row even">1</li>
|
||||
<li class="row odd">2</li>
|
||||
<li class="row even">3</li>
|
||||
:samp:`t-att=mapping`
|
||||
if the parameter is a mapping, each (key, value) pair generates a new
|
||||
attribute and its value::
|
||||
|
||||
<div t-att="{'a': 1, 'b': 2}"/>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<div a="1" b="2"></div>
|
||||
:samp:`t-att=pair`
|
||||
if the parameter is a pair (tuple or array of 2 element), the first
|
||||
item of the pair is the name of the attribute and the second item is the
|
||||
value::
|
||||
|
||||
<div t-att="['a', 'b']"/>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<div a="b"></div>
|
||||
|
||||
setting variables
|
||||
=================
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a
|
||||
computation (to use it multiple times), give a piece of data a clearer name,
|
||||
...
|
||||
|
||||
This is done via the ``set`` directive, which takes the name of the variable
|
||||
to create. The value to set can be provided in two ways:
|
||||
|
||||
* a ``t-value`` attribute containing an expression, and the result of its
|
||||
evaluation will be set::
|
||||
|
||||
<t t-set="foo" t-value="2 + 1"/>
|
||||
<t t-esc="foo"/>
|
||||
|
||||
will print ``3``
|
||||
* if there is no ``t-value`` attribute, the node's body is rendered and set
|
||||
as the variable's value::
|
||||
|
||||
<t t-set="foo">
|
||||
<li>ok</li>
|
||||
</t>
|
||||
<t t-esc="foo"/>
|
||||
|
||||
will generate ``<li>ok</li>`` (the content is escaped as we
|
||||
used the ``esc`` directive)
|
||||
|
||||
.. note:: using the result of this operation is a significant use-case for
|
||||
the ``raw`` directive.
|
||||
|
||||
calling sub-templates
|
||||
=====================
|
||||
|
||||
QWeb templates can be used for top-level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
templates) using the ``t-call`` directive::
|
||||
|
||||
<t t-call="other-template"/>
|
||||
|
||||
This calls the named template with the execution context of the parent, if
|
||||
``other_template`` is defined as::
|
||||
|
||||
<p><t t-value="var"/></p>
|
||||
|
||||
the call above will be rendered as ``<p/>`` (no content), but::
|
||||
|
||||
<t t-set="var" t-value="1"/>
|
||||
<t t-call="other-template"/>
|
||||
|
||||
will be rendered as ``<p>1</p>``.
|
||||
|
||||
However this has the problem of being visible from outside the ``t-call``.
|
||||
Alternatively, content set in the body of the ``call`` directive will be
|
||||
evaluated *before* calling the sub-template, and can alter a local context::
|
||||
|
||||
<t t-call="other-template">
|
||||
<t t-set="var" t-value="1"/>
|
||||
</t>
|
||||
<!-- "var" does not exist here -->
|
||||
|
||||
The body of the ``call`` directive can be arbitrarily complex (not just
|
||||
``set`` directives), and its rendered form will be available within the called
|
||||
template as a magical ``0`` variable::
|
||||
|
||||
<div>
|
||||
This template was called with content:
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
|
||||
being called thus::
|
||||
|
||||
<t t-call="other-template">
|
||||
<em>content</em>
|
||||
</t>
|
||||
|
||||
will result in::
|
||||
|
||||
<div>
|
||||
This template was called with content:
|
||||
<em>content</em>
|
||||
</div>
|
||||
|
||||
Python
|
||||
======
|
||||
|
||||
Exclusive directives
|
||||
--------------------
|
||||
|
||||
Asset bundles
|
||||
'''''''''''''
|
||||
|
||||
.. todo:: have fme write these up because I've no idea how they work
|
||||
|
||||
"smart records" fields formatting
|
||||
'''''''''''''''''''''''''''''''''
|
||||
|
||||
The ``t-field`` directive can only be used when performing field access
|
||||
(``a.b``) on a "smart" record (result of the ``browse`` method). It is able
|
||||
to automatically format based on field type, and is integrated in the
|
||||
website's rich text edition.
|
||||
|
||||
``t-options`` can be used to customize fields, the most common option
|
||||
is ``widget``, other options are field- or widget-dependent.
|
||||
|
||||
Debugging
|
||||
---------
|
||||
|
||||
``t-debug``
|
||||
invokes a debugger using PDB's ``set_trace`` API. The parameter should
|
||||
be the name of a module, on which a ``set_trace`` method is called::
|
||||
|
||||
<t t-debug="pdb"/>
|
||||
|
||||
is equivalent to ``importlib.import_module("pdb").set_trace()``
|
||||
|
||||
Helpers
|
||||
-------
|
||||
|
||||
Request-based
|
||||
'''''''''''''
|
||||
|
||||
Most Python-side uses of QWeb are in controllers (and during HTTP requests),
|
||||
in which case templates stored in the database (as
|
||||
:ref:`views <reference/views/qweb>`) can be trivially rendered by calling
|
||||
:meth:`odoo.http.HttpRequest.render`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = http.request.render('my-template', {
|
||||
'context_value': 42
|
||||
})
|
||||
|
||||
This automatically creates a :class:`~odoo.http.Response` object which can
|
||||
be returned from the controller (or further customized to suit).
|
||||
|
||||
View-based
|
||||
''''''''''
|
||||
|
||||
At a deeper level than the previous helper is the ``render`` method on
|
||||
``ir.ui.view``:
|
||||
|
||||
.. py:method:: render(cr, uid, id[, values][, engine='ir.qweb][, context])
|
||||
|
||||
Renders a QWeb view/template by database id or :term:`external id`.
|
||||
Templates are automatically loaded from ``ir.ui.view`` records.
|
||||
|
||||
Sets up a number of default values in the rendering context:
|
||||
|
||||
``request``
|
||||
the current :class:`~odoo.http.WebRequest` object, if any
|
||||
``debug``
|
||||
whether the current request (if any) is in ``debug`` mode
|
||||
:func:`quote_plus <werkzeug.urls.url_quote_plus>`
|
||||
url-encoding utility function
|
||||
:mod:`json`
|
||||
the corresponding standard library module
|
||||
:mod:`time`
|
||||
the corresponding standard library module
|
||||
:mod:`datetime`
|
||||
the corresponding standard library module
|
||||
`relativedelta <https://labix.org/python-dateutil#head-ba5ffd4df8111d1b83fc194b97ebecf837add454>`_
|
||||
see module
|
||||
``keep_query``
|
||||
the ``keep_query`` helper function
|
||||
|
||||
:param values: context values to pass to QWeb for rendering
|
||||
:param str engine: name of the Odoo model to use for rendering, can be
|
||||
used to expand or customize QWeb locally (by creating
|
||||
a "new" qweb based on ``ir.qweb`` with alterations)
|
||||
|
||||
.. _reference/qweb/javascript:
|
||||
|
||||
.. todo:: the members below are no longer relevant, section to rewrite
|
||||
|
||||
.. API
|
||||
.. ---
|
||||
|
||||
.. It is also possible to use the ``ir.qweb`` model directly (and extend it, and
|
||||
.. inherit from it):
|
||||
|
||||
.. .. automodule:: odoo.addons.base.ir.ir_qweb
|
||||
.. :members: QWeb, QWebContext, FieldConverter, QwebWidget
|
||||
|
||||
Javascript
|
||||
==========
|
||||
|
||||
Exclusive directives
|
||||
--------------------
|
||||
|
||||
Defining templates
|
||||
''''''''''''''''''
|
||||
|
||||
The ``t-name`` directive can only be placed at the top-level of a template
|
||||
file (direct children to the document root)::
|
||||
|
||||
<templates>
|
||||
<t t-name="template-name">
|
||||
<!-- template code -->
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
It takes no other parameter, but can be used with a ``<t>`` element or any
|
||||
other. With a ``<t>`` element, the ``<t>`` should have a single child.
|
||||
|
||||
The template name is an arbitrary string, although when multiple templates
|
||||
are related (e.g. called sub-templates) it is customary to use dot-separated
|
||||
names to indicate hierarchical relationships.
|
||||
|
||||
Template inheritance
|
||||
''''''''''''''''''''
|
||||
|
||||
Template inheritance is used to either:
|
||||
- Alter existing templates in-place, e.g. to add information to templates
|
||||
created by other modules.
|
||||
- Create a new template from a given parent template
|
||||
|
||||
Template inheritance is performed via the use of two directives:
|
||||
- ``t-inherit`` which is the name of the template to inherit from,
|
||||
- ``t-inherit-mode`` which is the behaviour of the inheritance: it can either be
|
||||
set to ``primary`` to create a new child template from the parented one or
|
||||
to ``extension`` to alter the parent template in place.
|
||||
|
||||
An optional ``t-name`` directive can also be specified. It will be the name of
|
||||
the newly created template if used in primary mode, else it will be added as a
|
||||
comment on the transformed template to help retrace inheritances.
|
||||
|
||||
For the inheritance itself, the changes are done using xpaths directives.
|
||||
See the XPATH_ documentation for the complete set of available instructions.
|
||||
|
||||
Primary inheritance (child template)::
|
||||
|
||||
<t t-name="child.template" t-inherit="base.template" t-inherit-mode="primary">
|
||||
<xpath expr="//ul" position="inside">
|
||||
<li>new element</li>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
Extension inheritance (in-place transformation)::
|
||||
|
||||
<t t-inherit="base.template" t-inherit-mode="extension">
|
||||
<xpath expr="//tr[1]" position="after">
|
||||
<tr><td>new cell</td></tr>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
Old inheritance mechanism (deprecated)
|
||||
''''''''''''''''''''''''''''''''''''''
|
||||
|
||||
Template inheritance is performed via the ``t-extend`` directive which takes
|
||||
the name of the template to alter as parameter.
|
||||
|
||||
The directive ``t-extend`` will act as a primary inheritance when combined with
|
||||
``t-name`` and as an extension one when used alone.
|
||||
|
||||
In both cases the alteration is then performed with any number of ``t-jquery``
|
||||
sub-directives::
|
||||
|
||||
<t t-extend="base.template">
|
||||
<t t-jquery="ul" t-operation="append">
|
||||
<li>new element</li>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
The ``t-jquery`` directives takes a `CSS selector`_. This selector is used
|
||||
on the extended template to select *context nodes* to which the specified
|
||||
``t-operation`` is applied:
|
||||
|
||||
``append``
|
||||
the node's body is appended at the end of the context node (after the
|
||||
context node's last child)
|
||||
``prepend``
|
||||
the node's body is prepended to the context node (inserted before the
|
||||
context node's first child)
|
||||
``before``
|
||||
the node's body is inserted right before the context node
|
||||
``after``
|
||||
the node's body is inserted right after the context node
|
||||
``inner``
|
||||
the node's body replaces the context node's children
|
||||
``replace``
|
||||
the node's body is used to replace the context node itself
|
||||
``attributes``
|
||||
the nodes's body should be any number of ``attribute`` elements,
|
||||
each with a ``name`` attribute and some textual content, the named
|
||||
attribute of the context node will be set to the specified value
|
||||
(either replaced if it already existed or added if not)
|
||||
No operation
|
||||
if no ``t-operation`` is specified, the template body is interpreted as
|
||||
javascript code and executed with the context node as ``this``
|
||||
|
||||
.. warning:: while much more powerful than other operations, this mode is
|
||||
also much harder to debug and maintain, it is recommended to
|
||||
avoid it
|
||||
|
||||
debugging
|
||||
---------
|
||||
|
||||
The javascript QWeb implementation provides a few debugging hooks:
|
||||
|
||||
``t-log``
|
||||
takes an expression parameter, evaluates the expression during rendering
|
||||
and logs its result with ``console.log``::
|
||||
|
||||
<t t-set="foo" t-value="42"/>
|
||||
<t t-log="foo"/>
|
||||
|
||||
will print ``42`` to the console
|
||||
``t-debug``
|
||||
triggers a debugger breakpoint during template rendering::
|
||||
|
||||
<t t-if="a_test">
|
||||
<t t-debug="">
|
||||
</t>
|
||||
|
||||
will stop execution if debugging is active (exact condition depend on the
|
||||
browser and its development tools)
|
||||
``t-js``
|
||||
the node's body is javascript code executed during template rendering.
|
||||
Takes a ``context`` parameter, which is the name under which the rendering
|
||||
context will be available in the ``t-js``'s body::
|
||||
|
||||
<t t-set="foo" t-value="42"/>
|
||||
<t t-js="ctx">
|
||||
console.log("Foo is", ctx.foo);
|
||||
</t>
|
||||
|
||||
Helpers
|
||||
-------
|
||||
|
||||
.. js:attribute:: core.qweb
|
||||
|
||||
(core is the ``web.core`` module) An instance of :js:class:`QWeb2.Engine` with all module-defined template
|
||||
files loaded, and references to standard helper objects ``_``
|
||||
(underscore), ``_t`` (translation function) and JSON_.
|
||||
|
||||
:js:func:`core.qweb.render <QWeb2.Engine.render>` can be used to
|
||||
easily render basic module templates
|
||||
|
||||
.. _reference/qweb/api:
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. js:class:: QWeb2.Engine
|
||||
|
||||
The QWeb "renderer", handles most of QWeb's logic (loading,
|
||||
parsing, compiling and rendering templates).
|
||||
|
||||
Odoo Web instantiates one for the user in the core module, and
|
||||
exports it to ``core.qweb``. It also loads all the template files
|
||||
of the various modules into that QWeb instance.
|
||||
|
||||
A :js:class:`QWeb2.Engine` also serves as a "template namespace".
|
||||
|
||||
.. js:function:: QWeb2.Engine.render(template[, context])
|
||||
|
||||
Renders a previously loaded template to a String, using
|
||||
``context`` (if provided) to find the variables accessed
|
||||
during template rendering (e.g. strings to display).
|
||||
|
||||
:param String template: the name of the template to render
|
||||
:param Object context: the basic namespace to use for template
|
||||
rendering
|
||||
:returns: String
|
||||
|
||||
The engine exposes an other method which may be useful in some
|
||||
cases (e.g. if you need a separate template namespace with, in
|
||||
Odoo Web, Kanban views get their own :js:class:`QWeb2.Engine`
|
||||
instance so their templates don't collide with more general
|
||||
"module" templates):
|
||||
|
||||
.. js:function:: QWeb2.Engine.add_template(templates)
|
||||
|
||||
Loads a template file (a collection of templates) in the QWeb
|
||||
instance. The templates can be specified as:
|
||||
|
||||
An XML string
|
||||
QWeb will attempt to parse it to an XML document then load
|
||||
it.
|
||||
|
||||
A URL
|
||||
QWeb will attempt to download the URL content, then load
|
||||
the resulting XML string.
|
||||
|
||||
A ``Document`` or ``Node``
|
||||
QWeb will traverse the first level of the document (the
|
||||
child nodes of the provided root) and load any named
|
||||
template or template override.
|
||||
|
||||
:type templates: String | Document | Node
|
||||
|
||||
A :js:class:`QWeb2.Engine` also exposes various attributes for
|
||||
behavior customization:
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.prefix
|
||||
|
||||
Prefix used to recognize directives during parsing. A string. By
|
||||
default, ``t``.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.debug
|
||||
|
||||
Boolean flag putting the engine in "debug mode". Normally,
|
||||
QWeb intercepts any error raised during template execution. In
|
||||
debug mode, it leaves all exceptions go through without
|
||||
intercepting them.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.jQuery
|
||||
|
||||
The jQuery instance used during template inheritance processing.
|
||||
Defaults to ``window.jQuery``.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.preprocess_node
|
||||
|
||||
A ``Function``. If present, called before compiling each DOM
|
||||
node to template code. In Odoo Web, this is used to
|
||||
automatically translate text content and some attributes in
|
||||
templates. Defaults to ``null``.
|
||||
|
||||
.. [#genshif] it is similar in that to Genshi_, although it does not use (and
|
||||
has no support for) `XML namespaces`_
|
||||
|
||||
.. [#othertemplates] although it uses a few others, either for historical
|
||||
reasons or because they remain better fits for the
|
||||
use case. Odoo 9.0 still depends on Jinja_ and Mako_.
|
||||
|
||||
.. _templating:
|
||||
https://en.wikipedia.org/wiki/Template_processor
|
||||
|
||||
.. _Jinja: http://jinja.pocoo.org
|
||||
.. _Mako: https://www.makotemplates.org
|
||||
.. _Genshi: https://genshi.edgewall.org
|
||||
.. _XML namespaces: https://en.wikipedia.org/wiki/XML_namespace
|
||||
.. _HTML: https://en.wikipedia.org/wiki/HTML
|
||||
.. _XSS: https://en.wikipedia.org/wiki/Cross-site_scripting
|
||||
.. _JSON: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
|
||||
.. _CSS selector: https://api.jquery.com/category/selectors/
|
||||
.. _XPATH: https://developer.mozilla.org/en-US/docs/Web/XPath
|
||||
@@ -0,0 +1,353 @@
|
||||
|
||||
.. highlight:: xml
|
||||
|
||||
.. _reference/reports:
|
||||
|
||||
============
|
||||
QWeb Reports
|
||||
============
|
||||
|
||||
Reports are written in HTML/QWeb, like all regular views in Odoo. You can use
|
||||
the usual :ref:`QWeb control flow tools <reference/qweb>`. The PDF rendering
|
||||
itself is performed by wkhtmltopdf_.
|
||||
|
||||
If you want to create a report on a certain model, you will need to define
|
||||
this :ref:`reference/reports/report` and the
|
||||
:ref:`reference/reports/templates` it will use. If you wish, you can also
|
||||
specify a specific :ref:`reference/reports/paper_formats` for this
|
||||
report. Finally, if you need access to more than your model, you can define a
|
||||
:ref:`reference/reports/custom_reports` class that gives you access to more
|
||||
models and records in the template.
|
||||
|
||||
.. _reference/reports/report:
|
||||
|
||||
Report
|
||||
======
|
||||
|
||||
Every report must be declared by a :ref:`report action
|
||||
<reference/actions/report>`.
|
||||
|
||||
For simplicity, a shortcut ``<report>`` element is available to define a
|
||||
report, rather than have to set up :ref:`the action
|
||||
<reference/actions/report>` and its surroundings manually. That ``<report>``
|
||||
can take the following attributes:
|
||||
|
||||
``id``
|
||||
the generated record's :term:`external id`
|
||||
``string`` (mapping to ``IrActionsReport.name`` field)
|
||||
used as the file name if ``print_report_name`` is not specified.
|
||||
Otherwise, only useful as a mnemonic/description of the report
|
||||
when looking for one in a list of some sort
|
||||
``name`` (mandatory) (mapping to ``IrActionsReport.report_name`` field)
|
||||
the name of the template used to render the report
|
||||
``model`` (mandatory)
|
||||
the model your report will be about
|
||||
``report_type`` (mandatory)
|
||||
either ``qweb-pdf`` for PDF reports or ``qweb-html`` for HTML
|
||||
``print_report_name``
|
||||
python expression defining the name of the report.
|
||||
``groups``
|
||||
:class:`~odoo.fields.Many2many` field to the groups allowed to view/use
|
||||
the current report
|
||||
``attachment_use``
|
||||
if set to True, the report will be stored as an attachment of the record
|
||||
using the name generated by the ``attachment`` expression; you can use
|
||||
this if you need your report to be generated only once (for legal reasons,
|
||||
for example)
|
||||
``attachment``
|
||||
python expression that defines the name of the report; the record is
|
||||
acessible as the variable ``object``
|
||||
``paperformat``
|
||||
external id of the paperformat you wish to use (defaults to the company's
|
||||
paperformat if not specified)
|
||||
|
||||
Example::
|
||||
|
||||
<report
|
||||
id="account_invoices"
|
||||
model="account.invoice"
|
||||
string="Invoices"
|
||||
name="account.report_invoice"
|
||||
report_type="qweb-pdf"
|
||||
print_report_name="object._get_report_filename()"
|
||||
attachment_use="True"
|
||||
attachment="(object.state in ('open','paid')) and
|
||||
('INV'+(object.number or '').replace('/','')+'.pdf')"
|
||||
/>
|
||||
|
||||
.. _reference/reports/templates:
|
||||
|
||||
Report template
|
||||
===============
|
||||
|
||||
|
||||
Minimal viable template
|
||||
-----------------------
|
||||
|
||||
A minimal template would look like::
|
||||
|
||||
<template id="report_invoice">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>Report title</h2>
|
||||
<p>This object's name is <span t-field="o.name"/></p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
Calling ``external_layout`` will add the default header and footer on your
|
||||
report. The PDF body will be the content inside the ``<div
|
||||
class="page">``. The template's ``id`` must be the name specified in the
|
||||
report declaration; for example ``account.report_invoice`` for the above
|
||||
report. Since this is a QWeb template, you can access all the fields of the
|
||||
``docs`` objects received by the template.
|
||||
|
||||
There are some specific variables accessible in reports, mainly:
|
||||
|
||||
``docs``
|
||||
records for the current report
|
||||
``doc_ids``
|
||||
list of ids for the ``docs`` records
|
||||
``doc_model``
|
||||
model for the ``docs`` records
|
||||
``time``
|
||||
a reference to :mod:`python:time` from the Python standard library
|
||||
``user``
|
||||
``res.user`` record for the user printing the report
|
||||
``res_company``
|
||||
record for the current ``user``'s company
|
||||
|
||||
If you wish to access other records/models in the template, you will need
|
||||
:ref:`a custom report <reference/reports/custom_reports>`.
|
||||
|
||||
Translatable Templates
|
||||
----------------------
|
||||
|
||||
If you wish to translate reports (to the language of a partner, for example),
|
||||
you need to define two templates:
|
||||
|
||||
* The main report template
|
||||
* The translatable document
|
||||
|
||||
You can then call the translatable document from your main template with the attribute
|
||||
``t-lang`` set to a language code (for example ``fr`` or ``en_US``) or to a record field.
|
||||
You will also need to re-browse the related records with the proper context if you use
|
||||
fields that are translatable (like country names, sales conditions, etc.)
|
||||
|
||||
.. warning::
|
||||
|
||||
If your report template does not use translatable record fields, re-browsing the record
|
||||
in another language is *not* necessary and will impact performances.
|
||||
|
||||
For example, let's look at the Sale Order report from the Sale module::
|
||||
|
||||
<!-- Main template -->
|
||||
<template id="report_saleorder">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="sale.report_saleorder_document" t-lang="doc.partner_id.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Translatable template -->
|
||||
<template id="report_saleorder_document">
|
||||
<!-- Re-browse of the record with the partner lang -->
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" />
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<div class="oe_structure"/>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<strong t-if="doc.partner_shipping_id == doc.partner_invoice_id">Invoice and shipping address:</strong>
|
||||
<strong t-if="doc.partner_shipping_id != doc.partner_invoice_id">Invoice address:</strong>
|
||||
<div t-field="doc.partner_invoice_id" t-options="{"no_marker": True}"/>
|
||||
<...>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
|
||||
The main template calls the translatable template with ``doc.partner_id.lang`` as a
|
||||
``t-lang`` parameter, so it will be rendered in the language of the partner. This way,
|
||||
each Sale Order will be printed in the language of the corresponding customer. If you wish
|
||||
to translate only the body of the document, but keep the header and footer in a default
|
||||
language, you could call the report's external layout this way::
|
||||
|
||||
<t t-call="web.external_layout" t-lang="en_US">
|
||||
|
||||
.. tip::
|
||||
|
||||
Please take note that this works only when calling external templates, you will not be
|
||||
able to translate part of a document by setting a ``t-lang`` attribute on an xml node other
|
||||
than ``t-call``. If you wish to translate part of a template, you can create an external
|
||||
template with this partial template and call it from the main one with the ``t-lang``
|
||||
attribute.
|
||||
|
||||
|
||||
Barcodes
|
||||
--------
|
||||
|
||||
Barcodes are images returned by a controller and can easily be embedded in
|
||||
reports thanks to the QWeb syntax (e.g. see :ref:`reference/qweb/attributes`):
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img t-att-src="'/report/barcode/QR/%s' % 'My text in qr code'"/>
|
||||
|
||||
More parameters can be passed as a query string
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img t-att-src="'/report/barcode/?
|
||||
type=%s&value=%s&width=%s&height=%s'%('QR', 'text', 200, 200)"/>
|
||||
|
||||
|
||||
Useful Remarks
|
||||
--------------
|
||||
* Twitter Bootstrap and FontAwesome classes can be used in your report
|
||||
template
|
||||
* Local CSS can be put directly in the template
|
||||
* Global CSS can be inserted in the main report layout by inheriting its
|
||||
template and inserting your CSS::
|
||||
|
||||
<template id="report_saleorder_style" inherit_id="report.style">
|
||||
<xpath expr=".">
|
||||
<t>
|
||||
.example-css-class {
|
||||
background-color: red;
|
||||
}
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
* If it appears that your PDF report is missing the styles, please check
|
||||
:ref:`these instructions <reference/backend/reporting/printed-reports/pdf-without-styles>`.
|
||||
|
||||
.. _reference/reports/paper_formats:
|
||||
|
||||
Paper Format
|
||||
============
|
||||
|
||||
Paper formats are records of ``report.paperformat`` and can contain the
|
||||
following attributes:
|
||||
|
||||
``name`` (mandatory)
|
||||
only useful as a mnemonic/description of the report when looking for one
|
||||
in a list of some sort
|
||||
``description``
|
||||
a small description of your format
|
||||
``format``
|
||||
either a predefined format (A0 to A9, B0 to B10, Legal, Letter,
|
||||
Tabloid,...) or ``custom``; A4 by default. You cannot use a non-custom
|
||||
format if you define the page dimensions.
|
||||
``dpi``
|
||||
output DPI; 90 by default
|
||||
``margin_top``, ``margin_bottom``, ``margin_left``, ``margin_right``
|
||||
margin sizes in mm
|
||||
``page_height``, ``page_width``
|
||||
page dimensions in mm
|
||||
``orientation``
|
||||
Landscape or Portrait
|
||||
``header_line``
|
||||
boolean to display a header line
|
||||
``header_spacing``
|
||||
header spacing in mm
|
||||
|
||||
Example::
|
||||
|
||||
<record id="paperformat_frenchcheck" model="report.paperformat">
|
||||
<field name="name">French Bank Check</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="format">custom</field>
|
||||
<field name="page_height">80</field>
|
||||
<field name="page_width">175</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">3</field>
|
||||
<field name="margin_bottom">3</field>
|
||||
<field name="margin_left">3</field>
|
||||
<field name="margin_right">3</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">3</field>
|
||||
<field name="dpi">80</field>
|
||||
</record>
|
||||
|
||||
.. _reference/reports/custom_reports:
|
||||
|
||||
Custom Reports
|
||||
==============
|
||||
|
||||
The report model has a default ``get_html`` function that looks for a model
|
||||
named :samp:`report.{module.report_name}`. If it exists, it will use it to
|
||||
call the QWeb engine; otherwise a generic function will be used. If you wish
|
||||
to customize your reports by including more things in the template (like
|
||||
records of others models, for example), you can define this model, overwrite
|
||||
the function ``_get_report_values`` and pass objects in the ``docargs`` dictionary:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
class ParticularReport(models.AbstractModel):
|
||||
_name = 'report.module.report_name'
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
report_obj = self.env['ir.actions.report']
|
||||
report = report_obj._get_report_from_name('module.report_name')
|
||||
docargs = {
|
||||
'doc_ids': docids,
|
||||
'doc_model': report.model,
|
||||
'docs': self,
|
||||
}
|
||||
return docargs
|
||||
|
||||
.. _reference/reports/custom_fonts:
|
||||
|
||||
Custom fonts
|
||||
============
|
||||
If you want to use custom fonts you will need to add your custom font and the related less/CSS to the ``web.reports_assets_common`` assets bundle.
|
||||
Adding your custom font(s) to ``web.assets_common`` or ``web.assets_backend`` will not make your font available in QWeb reports.
|
||||
|
||||
Example::
|
||||
|
||||
<template id="report_assets_common_custom_fonts" name="Custom QWeb fonts" inherit_id="web.report_assets_common">
|
||||
<xpath expr="." position="inside">
|
||||
<link href="/your_module/static/src/less/fonts.less" rel="stylesheet" type="text/less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
You will need to define your ``@font-face`` within this less file, even if you've used in another assets bundle (other than ``web.reports_assets_common``).
|
||||
|
||||
Example::
|
||||
|
||||
@font-face {
|
||||
font-family: 'MonixBold';
|
||||
src: local('MonixBold'), local('MonixBold'), url(/your_module/static/src/fonts/MonixBold-Regular.otf) format('opentype');
|
||||
}
|
||||
|
||||
.h1-title-big {
|
||||
font-family: MonixBold;
|
||||
font-size: 60px;
|
||||
color: #3399cc;
|
||||
}
|
||||
|
||||
After you've added the less into your assets bundle you can use the classes - in this example ``h1-title-big`` - in your custom QWeb report.
|
||||
|
||||
Reports are web pages
|
||||
=====================
|
||||
|
||||
Reports are dynamically generated by the report module and can be accessed
|
||||
directly via URL:
|
||||
|
||||
For example, you can access a Sale Order report in html mode by going to
|
||||
\http://<server-address>/report/html/sale.report_saleorder/38
|
||||
|
||||
Or you can access the pdf version at
|
||||
\http://<server-address>/report/pdf/sale.report_saleorder/38
|
||||
|
||||
.. _wkhtmltopdf: https://wkhtmltopdf.org
|
||||
@@ -0,0 +1,388 @@
|
||||
|
||||
.. _reference/security:
|
||||
|
||||
================
|
||||
Security in Odoo
|
||||
================
|
||||
|
||||
Aside from manually managing access using custom code, Odoo provides two main
|
||||
data-driven mechanisms to manage or restrict access to data.
|
||||
|
||||
Both mechanisms are linked to specific users through *groups*: a user belongs
|
||||
to any number of groups, and security mechanisms are associated to groups,
|
||||
thus applying security mechamisms to users.
|
||||
|
||||
.. _reference/security/acl:
|
||||
|
||||
Access Control
|
||||
==============
|
||||
|
||||
Managed by the ``ir.model.access`` records, defines access to a whole model.
|
||||
|
||||
Each access control has a model to which it grants permissions, the
|
||||
permissions it grants and optionally a group.
|
||||
|
||||
Access controls are additive, for a given model a user has access all
|
||||
permissions granted to any of its groups: if the user belongs to one group
|
||||
which allows writing and another which allows deleting, they can both write
|
||||
and delete.
|
||||
|
||||
If no group is specified, the access control applies to all users, otherwise
|
||||
it only applies to the members of the given group.
|
||||
|
||||
Available permissions are creation (``perm_create``), searching and reading
|
||||
(``perm_read``), updating existing records (``perm_write``) and deleting
|
||||
existing records (``perm_unlink``)
|
||||
|
||||
.. _reference/security/rules:
|
||||
|
||||
Record Rules
|
||||
============
|
||||
|
||||
Record rules are conditions that records must satisfy for an operation
|
||||
(create, read, update or delete) to be allowed. It is applied record-by-record
|
||||
after access control has been applied.
|
||||
|
||||
A record rule has:
|
||||
|
||||
* a model on which it applies
|
||||
* a set of permissions to which it applies (e.g. if ``perm_read`` is set, the
|
||||
rule will only be checked when reading a record)
|
||||
* a set of user groups to which the rule applies, if no group is specified
|
||||
the rule is *global*
|
||||
* a :ref:`domain <reference/orm/domains>` used to check whether a given record
|
||||
matches the rule (and is accessible) or does not (and is not accessible).
|
||||
The domain is evaluated with two variables in context: ``user`` is the
|
||||
current user's record and ``time`` is the `time module`_
|
||||
|
||||
Global rules and group rules (rules restricted to specific groups versus
|
||||
groups applying to all users) are used quite differently:
|
||||
|
||||
* Global rules are subtractive, they *must all* be matched for a record to be
|
||||
accessible
|
||||
* Group rules are additive, if *any* of them matches (and all global rules
|
||||
match) then the record is accessible
|
||||
|
||||
This means the first *group rule* restricts access, but any further
|
||||
*group rule* expands it, while *global rules* can only ever restrict access
|
||||
(or have no effect).
|
||||
|
||||
.. warning:: record rules do not apply to the Superuser account
|
||||
:class: aphorism
|
||||
|
||||
.. _reference/security/fields:
|
||||
|
||||
Field Access
|
||||
============
|
||||
|
||||
An ORM :class:`~odoo.fields.Field` can have a ``groups`` attribute
|
||||
providing a list of groups (as a comma-separated string of
|
||||
:term:`external identifiers`).
|
||||
|
||||
If the current user is not in one of the listed groups, he will not have
|
||||
access to the field:
|
||||
|
||||
* restricted fields are automatically removed from requested views
|
||||
* restricted fields are removed from :meth:`~odoo.models.Model.fields_get`
|
||||
responses
|
||||
* attempts to (explicitly) read from or write to restricted fields results in
|
||||
an access error
|
||||
|
||||
.. todo::
|
||||
|
||||
field access groups apply to the Superuser in fields_get but not in
|
||||
read/write...
|
||||
|
||||
.. _time module: https://docs.python.org/3/library/time.html
|
||||
|
||||
|
||||
.. _reference/security/pitfalls:
|
||||
|
||||
Security Pitfalls
|
||||
=================
|
||||
|
||||
As a developer, it is important to understand the security mechanisms and avoid
|
||||
common mistakes leading to insecure code.
|
||||
|
||||
Unsafe Public Methods
|
||||
---------------------
|
||||
|
||||
Any public method can be executed via a :ref:`RPC call
|
||||
<webservices/odoo/calling_methods>` with the chosen parameters. The methods
|
||||
starting with a ``_`` are not callable from an action button or external API.
|
||||
|
||||
On public methods, the record on which a method is executed and the parameters
|
||||
can not be trusted, ACL being only verified during CRUD operations.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# this method is public and its arguments can not be trusted
|
||||
def action_done(self):
|
||||
if self.state == "draft" and self.user_has_groups('base.manager'):
|
||||
self._set_state("done")
|
||||
|
||||
# this method is private and can only be called from other python methods
|
||||
def _set_state(self, new_state):
|
||||
self.sudo().write({"state": new_state})
|
||||
|
||||
Making a method private is obviously not enough and care must be taken to use it
|
||||
properly.
|
||||
|
||||
Bypassing the ORM
|
||||
-----------------
|
||||
You should never use the database cursor directly when the ORM can do the same
|
||||
thing! By doing so you are bypassing all the ORM features, possibly the
|
||||
automated behaviours like translations, invalidation of fields, ``active``,
|
||||
access rights and so on.
|
||||
|
||||
And chances are that you are also making the code harder to read and probably
|
||||
less secure.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# very very wrong
|
||||
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
|
||||
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
|
||||
|
||||
# no injection, but still wrong
|
||||
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '\
|
||||
'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
|
||||
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
|
||||
|
||||
# better
|
||||
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
|
||||
|
||||
|
||||
SQL injections
|
||||
~~~~~~~~~~~~~~
|
||||
Care must be taken not to introduce SQL injections vulnerabilities when using
|
||||
manual SQL queries. The vulnerability is present when user input is either
|
||||
incorrectly filtered or badly quoted, allowing an attacker to introduce
|
||||
undesirable clauses to a SQL query (such as circumventing filters or
|
||||
executing ``UPDATE`` or ``DELETE`` commands).
|
||||
|
||||
The best way to be safe is to never, NEVER use Python string concatenation (+)
|
||||
or string parameters interpolation (%) to pass variables to a SQL query string.
|
||||
|
||||
The second reason, which is almost as important, is that it is the job of the
|
||||
database abstraction layer (psycopg2) to decide how to format query parameters,
|
||||
not your job! For example psycopg2 knows that when you pass a list of values
|
||||
it needs to format them as a comma-separated list, enclosed in parentheses !
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# the following is very bad:
|
||||
# - it's a SQL injection vulnerability
|
||||
# - it's unreadable
|
||||
# - it's not your job to format the list of ids
|
||||
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
|
||||
'WHERE parent_id IN ('+','.join(map(str, ids))+')')
|
||||
|
||||
# better
|
||||
self.env.cr.execute('SELECT DISTINCT child_id '\
|
||||
'FROM account_account_consol_rel '\
|
||||
'WHERE parent_id IN %s',
|
||||
(tuple(ids),))
|
||||
|
||||
This is very important, so please be careful also when refactoring, and most
|
||||
importantly do not copy these patterns!
|
||||
|
||||
Here is a memorable example to help you remember what the issue is about (but
|
||||
do not copy the code there). Before continuing, please be sure to read the
|
||||
online documentation of pyscopg2 to learn of to use it properly:
|
||||
|
||||
- `The problem with query parameters <http://initd.org/psycopg/docs/usage.html#the-problem-with-the-query-parameters>`_
|
||||
- `How to pass parameters with psycopg2 <http://initd.org/psycopg/docs/usage.html#passing-parameters-to-sql-queries>`_
|
||||
- `Advanced parameter types <http://initd.org/psycopg/docs/usage.html#adaptation-of-python-values-to-sql-types>`_
|
||||
- `Psycopg documentation <https://www.psycopg.org/docs/sql.html>`_
|
||||
|
||||
Unescaped field content
|
||||
-----------------------
|
||||
|
||||
When rendering content using JavaScript and XML, one may be tempted to use
|
||||
a ``t-raw`` to display rich-text content. This should be avoided as a frequent
|
||||
`XSS <https://en.wikipedia.org/wiki/Cross-site_scripting>`_ vector.
|
||||
|
||||
It is very hard to control the integrity of the data from the computation until
|
||||
the final integration in the browser DOM. A ``t-raw`` that is correctly escaped
|
||||
at the time of introduction may no longer be safe at the next bugfix or
|
||||
refactoring.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
QWeb.render('insecure_template', {
|
||||
info_message: "You have an <strong>important</strong> notification",
|
||||
})
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<div t-name="insecure_template">
|
||||
<div id="information-bar"><t t-raw="info_message" /></div>
|
||||
</div>
|
||||
|
||||
The above code may feel safe as the message content is controlled but is a bad
|
||||
practice that may lead to unexpected security vulnerabilities once this code
|
||||
evolves in the future.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// XSS possible with unescaped user provided content !
|
||||
QWeb.render('insecure_template', {
|
||||
info_message: "You have an <strong>important</strong> notification on " \
|
||||
+ "the product <strong>" + product.name + "</strong>",
|
||||
})
|
||||
|
||||
While formatting the template differently would prevent such vulnerabilities.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
QWeb.render('secure_template', {
|
||||
message: "You have an important notification on the product:",
|
||||
subject: product.name
|
||||
})
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<div t-name="secure_template">
|
||||
<div id="information-bar">
|
||||
<div class="info"><t t-esc="message" /></div>
|
||||
<div class="subject"><t t-esc="subject" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
.. code-block:: css
|
||||
|
||||
.subject {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
Escaping vs Sanitizing
|
||||
----------------------
|
||||
|
||||
.. important::
|
||||
|
||||
Escaping is always 100% mandatory when you mix data and code, no matter how
|
||||
safe the data
|
||||
|
||||
**Escaping** converts *TEXT* to *CODE*. It is absolutely mandatory to do it
|
||||
every time you mix *DATA/TEXT* with *CODE* (e.g. generating HTML or python code
|
||||
to be evaluated inside a `safe_eval`), because *CODE* always requires *TEXT* to
|
||||
be encoded. It is critical for security, but it's also a question of
|
||||
correctness. Even when there is no security risk (because the text is 100%
|
||||
guarantee to be safe or trusted), it is still required (e.g. to avoid breaking
|
||||
the layout in generated HTML).
|
||||
|
||||
Escaping will never break any feature, as long as the developer identifies which
|
||||
variable contains *TEXT* and which contains *CODE*.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from odoo.tools import html_escape, html_sanitize
|
||||
>>> data = "<R&D>" # `data` is some TEXT coming from somewhere
|
||||
|
||||
# Escaping turns it into CODE, good!
|
||||
>>> code = html_escape(data)
|
||||
>>> code
|
||||
'<R&D>'
|
||||
|
||||
# Now you can mix it with other code...
|
||||
>>> self.message_post(body="<strong>%s</strong>" % code)
|
||||
|
||||
**Sanitizing** converts *CODE* to *SAFER CODE* (but not necessary *safe* code).
|
||||
It does not work on *TEXT*. Sanitizing is only necessary when *CODE* is
|
||||
untrusted, because it comes in full or in part from some user-provided data. If
|
||||
the user-provided data is in the form of *TEXT* (e.g. the content from a form
|
||||
filled by a user), and if that data was correctly escaped before putting it in
|
||||
*CODE*, then sanitizing is useless (but can still be done). If however, the
|
||||
user-provided data was **not escaped**, then sanitizing will **not** work as
|
||||
expected.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Sanitizing without escaping is BROKEN: data is corrupted!
|
||||
>>> html_sanitize(data)
|
||||
''
|
||||
|
||||
# Sanitizing *after* escaping is OK!
|
||||
>>> html_sanitize(code)
|
||||
'<p><R&D></p>'
|
||||
|
||||
Sanitizing can break features, depending on whether the *CODE* is expected to
|
||||
contain patterns that are not safe. That's why `fields.Html` and
|
||||
`tools.html_sanitize()` have options to fine-tune the level of sanitization for
|
||||
styles, etc. Those options have to be carefully considered depending on where
|
||||
the data comes from, and the desired features. The sanitization safety is
|
||||
balanced against sanitization breakages: the safer the sanitisation the more
|
||||
likely it is to break things.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>code = "<p class='text-warning'>Important Information</p>"
|
||||
# this will remove the style, which may break features
|
||||
# but is necessary if the source is untrusted
|
||||
>> html_sanitize(code, strip_classes=True)
|
||||
'<p>Important Information</p>'
|
||||
|
||||
Evaluating content
|
||||
------------------
|
||||
Some may want to ``eval`` to parse user provided content. Using ``eval`` should
|
||||
be avoided at all cost. A safer, sandboxed, method :class:`~odoo.tools.safe_eval`
|
||||
can be used instead but still gives tremendous capabilities to the user running
|
||||
it and must be reserved for trusted privileged users only as it breaks the
|
||||
barrier between code and data.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# very bad
|
||||
domain = eval(self.filter_domain)
|
||||
return self.search(domain)
|
||||
|
||||
# better but still not recommended
|
||||
from odoo.tools import safe_eval
|
||||
domain = safe_eval(self.filter_domain)
|
||||
return self.search(domain)
|
||||
|
||||
# good
|
||||
from ast import literal_eval
|
||||
domain = literal_eval(self.filter_domain)
|
||||
return self.search(domain)
|
||||
|
||||
Parsing content does not need ``eval``
|
||||
|
||||
========== ================== ================================
|
||||
Language Data type Suitable parser
|
||||
========== ================== ================================
|
||||
Python int, float, etc. int(), float()
|
||||
Javascript int, float, etc. parseInt(), parseFloat()
|
||||
Python dict json.loads(), ast.literal_eval()
|
||||
Javascript object, list, etc. JSON.parse()
|
||||
========== ================== ================================
|
||||
|
||||
Accessing object attributes
|
||||
---------------------------
|
||||
|
||||
If the values of a record needs to be retrieved or modified dynamically, one may
|
||||
want to use the ``getattr`` and ``setattr`` methods.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# unsafe retrieval of a field value
|
||||
def _get_state_value(self, res_id, state_field):
|
||||
record = self.sudo().browse(res_id)
|
||||
return getattr(record, state_field, False)
|
||||
|
||||
This code is however not safe as it allows to access any property of the record,
|
||||
including private attributes or methods.
|
||||
|
||||
The ``__getitem__`` of a recordset has been defined and accessing a dynamic
|
||||
field value can be easily achieved safely:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# better retrieval of a field value
|
||||
def _get_state_value(self, res_id, state_field):
|
||||
record = self.sudo().browse(res_id)
|
||||
return record[state_field]
|
||||
|
||||
The above method is obviously still too optimistic and additional verifications
|
||||
on the record id and field value must be done.
|
||||
@@ -0,0 +1,15 @@
|
||||
"id","country_id:id","name","code"
|
||||
state_au_1,au,"Australian Capital Territory","ACT"
|
||||
state_au_2,au,"New South Wales","NSW"
|
||||
state_au_3,au,"Northern Territory","NT"
|
||||
state_au_4,au,"Queensland","QLD"
|
||||
state_au_5,au,"South Australia","SA"
|
||||
state_au_6,au,"Tasmania","TAS"
|
||||
state_au_7,au,"Victoria","VIC"
|
||||
state_au_8,au,"Western Australia","WA"
|
||||
state_us_1,us,"Alabama","AL"
|
||||
state_us_2,us,"Alaska","AK"
|
||||
state_us_3,us,"Arizona","AZ"
|
||||
state_us_4,us,"Arkansas","AR"
|
||||
state_us_5,us,"California","CA"
|
||||
state_us_6,us,"Colorado","CO"
|
||||
|
@@ -0,0 +1,530 @@
|
||||
|
||||
.. _reference/testing:
|
||||
|
||||
|
||||
===============
|
||||
Testing Odoo
|
||||
===============
|
||||
|
||||
There are many ways to test an application. In Odoo, we have three kinds of
|
||||
tests
|
||||
|
||||
- Python unit tests (see `Testing Python code`_): useful for testing model business logic
|
||||
- JS unit tests (see `Testing JS code`_): useful to test the javascript code in isolation
|
||||
- Tours (see `Integration Testing`_): tours simulate a real situation. They ensures that the
|
||||
python and the javascript parts properly talk to each other.
|
||||
|
||||
Testing Python code
|
||||
===================
|
||||
|
||||
Odoo provides support for testing modules using unittest.
|
||||
|
||||
To write tests, simply define a ``tests`` sub-package in your module, it will
|
||||
be automatically inspected for test modules. Test modules should have a name
|
||||
starting with ``test_`` and should be imported from ``tests/__init__.py``,
|
||||
e.g.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
your_module
|
||||
|-- ...
|
||||
`-- tests
|
||||
|-- __init__.py
|
||||
|-- test_bar.py
|
||||
`-- test_foo.py
|
||||
|
||||
and ``__init__.py`` contains::
|
||||
|
||||
from . import test_foo, test_bar
|
||||
|
||||
.. warning::
|
||||
|
||||
test modules which are not imported from ``tests/__init__.py`` will not be
|
||||
run
|
||||
|
||||
The test runner will simply run any test case, as described in the official
|
||||
`unittest documentation`_, but Odoo provides a number of utilities and helpers
|
||||
related to testing Odoo content (modules, mainly):
|
||||
|
||||
.. autoclass:: odoo.tests.common.TransactionCase
|
||||
:members: browse_ref, ref
|
||||
|
||||
.. autoclass:: odoo.tests.common.SingleTransactionCase
|
||||
:members: browse_ref, ref
|
||||
|
||||
.. autoclass:: odoo.tests.common.SavepointCase
|
||||
|
||||
.. autoclass:: odoo.tests.common.HttpCase
|
||||
:members: browse_ref, ref, url_open, phantom_js
|
||||
|
||||
.. autofunction:: odoo.tests.common.tagged
|
||||
|
||||
By default, tests are run once right after the corresponding module has been
|
||||
installed. Test cases can also be configured to run after all modules have
|
||||
been installed, and not run right after the module installation:
|
||||
|
||||
.. autofunction:: odoo.tests.common.at_install
|
||||
|
||||
.. autofunction:: odoo.tests.common.post_install
|
||||
|
||||
The most common situation is to use
|
||||
:class:`~odoo.tests.common.TransactionCase` and test a property of a model
|
||||
in each method::
|
||||
|
||||
class TestModelA(common.TransactionCase):
|
||||
def test_some_action(self):
|
||||
record = self.env['model.a'].create({'field': 'value'})
|
||||
record.some_action()
|
||||
self.assertEqual(
|
||||
record.field,
|
||||
expected_field_value)
|
||||
|
||||
# other tests...
|
||||
|
||||
.. note::
|
||||
|
||||
Test methods must start with ``test_``
|
||||
|
||||
.. autoclass:: odoo.tests.common.Form
|
||||
:members:
|
||||
|
||||
.. autoclass:: odoo.tests.common.M2MProxy
|
||||
:members: add, remove, clear
|
||||
|
||||
.. autoclass:: odoo.tests.common.O2MProxy
|
||||
:members: new, edit, remove
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
Tests are automatically run when installing or updating modules if
|
||||
:option:`--test-enable <odoo-bin --test-enable>` was enabled when starting the
|
||||
Odoo server.
|
||||
|
||||
.. _unittest documentation: https://docs.python.org/3/library/unittest.html
|
||||
|
||||
Test selection
|
||||
--------------
|
||||
|
||||
In Odoo, Python tests can be tagged to facilitate the test selection when
|
||||
running tests.
|
||||
|
||||
Subclasses of :class:`odoo.tests.common.BaseCase` (usually through
|
||||
:class:`~odoo.tests.common.TransactionCase`,
|
||||
:class:`~odoo.tests.common.SavepointCase` or
|
||||
:class:`~odoo.tests.common.HttpCase`) are automatically tagged with
|
||||
``standard``, ``at_install`` and their source module's name by default.
|
||||
|
||||
Invocation
|
||||
^^^^^^^^^^
|
||||
|
||||
:option:`--test-tags <odoo-bin --test-tags>` can be used to select/filter tests
|
||||
to run on the command-line.
|
||||
|
||||
This option defaults to ``+standard`` meaning tests tagged ``standard``
|
||||
(explicitly or implicitly) will be run by default when starting Odoo
|
||||
with :option:`--test-enable <odoo-bin --test-enable>`.
|
||||
|
||||
When writing tests, the :func:`~odoo.tests.common.tagged` decorator can be
|
||||
used on **test classes** to add or remove tags.
|
||||
|
||||
The decorator's arguments are tag names, as strings.
|
||||
|
||||
.. danger:: :func:`~odoo.tests.common.tagged` is a class decorator, it has no
|
||||
effect on functions or methods
|
||||
|
||||
Tags can be prefixed with the minus (``-``) sign, to *remove* them instead of
|
||||
add or select them e.g. if you don't want your test to be executed by
|
||||
default you can remove the ``standard`` tag:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
@tagged('-standard', 'nice')
|
||||
class NiceTest(TransactionCase):
|
||||
...
|
||||
|
||||
This test will not be selected by default, to run it the relevant tag will
|
||||
have to be selected explicitely:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags nice
|
||||
|
||||
Note that only the tests tagged ``nice`` are going to be executed. To run
|
||||
*both* ``nice`` and ``standard`` tests, provide multiple values to
|
||||
:option:`--test-tags <odoo-bin --test-tags>`: on the command-line, values
|
||||
are *additive* (you're selecting all tests with *any* of the specified tags)
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags nice,standard
|
||||
|
||||
The config switch parameter also accepts the ``+`` and ``-`` prefixes. The
|
||||
``+`` prefix is implied and therefore, totaly optional. The ``-`` (minus)
|
||||
prefix is made to deselect tests tagged with the prefixed tags, even if they
|
||||
are selected by other specified tags e.g. if there are ``standard`` tests which
|
||||
are also tagged as ``slow`` you can run all standard tests *except* the slow
|
||||
ones:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags 'standard,-slow'
|
||||
|
||||
When you write a test that does not inherit from the
|
||||
:class:`~odoo.tests.common.BaseCase`, this test will not have the default tags,
|
||||
you have to add them explicitely to have the test included in the default test
|
||||
suite. This is a common issue when using a simple ``unittest.TestCase`` as
|
||||
they're not going to get run:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import unittest
|
||||
from odoo.tests import tagged
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class SmallTest(unittest.TestCase):
|
||||
...
|
||||
|
||||
Special tags
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- ``standard``: All Odoo tests that inherit from
|
||||
:class:`~odoo.tests.common.BaseCase` are implicitely tagged standard.
|
||||
:option:`--test-tags <odoo-bin --test-tags>` also defaults to ``standard``.
|
||||
|
||||
That means untagged test will be executed by default when tests are enabled.
|
||||
- ``at_install``: Means that the test will be executed right after the module
|
||||
installation and before other modules are installed. This is a default
|
||||
implicit tag.
|
||||
- ``post_install``: Means that the test will be executed after all the modules
|
||||
are installed. This is what you want for HttpCase tests most of the time.
|
||||
|
||||
Note that this is *not exclusive* with ``at_install``, however since you
|
||||
will generally not want both ``post_install`` is usually paired with
|
||||
``-at_install`` when tagging a test class.
|
||||
- *module_name*: Odoo tests classes extending
|
||||
:class:`~odoo.tests.common.BaseCase` are implicitely tagged with the
|
||||
technical name of their module. This allows easily selecting or excluding
|
||||
specific modules when testing e.g. if you want to only run tests from
|
||||
``stock_account``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags stock_account
|
||||
|
||||
Examples
|
||||
^^^^^^^^
|
||||
|
||||
.. important::
|
||||
|
||||
Tests will be executed only in the installed or updated modules. So
|
||||
modules have to be selected with the :option:`-u <odoo-bin -u>` or
|
||||
:option:`-i <odoo-bin -i>` switches. For simplicity, those switches are
|
||||
not specified in the examples below.
|
||||
|
||||
Run only the tests from the sale module:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags sale
|
||||
|
||||
Run the tests from the sale module but not the ones tagged as slow:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags 'sale,-slow'
|
||||
|
||||
Run only the tests from stock or tagged as slow:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin --test-enable --test-tags '-standard, slow, stock'
|
||||
|
||||
.. note:: ``-standard`` is implicit (not required), and present for clarity
|
||||
|
||||
Testing JS code
|
||||
===============
|
||||
|
||||
Testing a complex system is an important safeguard to prevent regressions and to
|
||||
guarantee that some basic functionality still works. Since Odoo has a non trivial
|
||||
codebase in Javascript, it is necessary to test it. In this section, we will
|
||||
discuss the practice of testing JS code in isolation: these tests stay in the
|
||||
browser, and are not supposed to reach the server.
|
||||
|
||||
Qunit test suite
|
||||
----------------
|
||||
|
||||
The Odoo framework uses the QUnit_ library testing framework as a test runner.
|
||||
QUnit defines the concepts of *tests* and *modules* (a set of related tests),
|
||||
and gives us a web based interface to execute the tests.
|
||||
|
||||
For example, here is what a pyUtils test could look like:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
QUnit.module('py_utils');
|
||||
|
||||
QUnit.test('simple arithmetic', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var result = pyUtils.py_eval("1 + 2");
|
||||
assert.strictEqual(result, 3, "should properly evaluate sum");
|
||||
result = pyUtils.py_eval("42 % 5");
|
||||
assert.strictEqual(result, 2, "should properly evaluate modulo operator");
|
||||
});
|
||||
|
||||
The main way to run the test suite is to have a running Odoo server, then
|
||||
navigate a web browser to ``/web/tests``. The test suite will then be executed
|
||||
by the web browser Javascript engine.
|
||||
|
||||
.. image:: ./images/tests.png
|
||||
:align: center
|
||||
|
||||
The web UI has many useful features: it can run only some submodules, or
|
||||
filter tests that match a string. It can show every assertions, failed or passed,
|
||||
rerun specific tests, ...
|
||||
|
||||
.. warning::
|
||||
|
||||
While the test suite is running, make sure that:
|
||||
|
||||
- your browser window is focused,
|
||||
- it is not zoomed in/out. It needs to have exactly 100% zoom level.
|
||||
|
||||
If this is not the case, some tests will fail, without a proper explanation.
|
||||
|
||||
Testing Infrastructure
|
||||
----------------------
|
||||
|
||||
Here is a high level overview of the most important parts of the testing
|
||||
infrastructure:
|
||||
|
||||
- there is an asset bundle named `web.js_tests_assets`_. This bundle contains
|
||||
the main code (assets common + assets backend), some libraries, the QUnit test
|
||||
runner, and some additional helper code
|
||||
|
||||
- another asset bundle, `web.qunit_suite`_, contains all the tests (and the
|
||||
js_tests_assets code). Almost all the test files should be added to this
|
||||
bundle
|
||||
|
||||
- there is a `controller`_ in web, mapped to the route */web/tests*. This controller
|
||||
simply renders the *web.qunit_suite* template.
|
||||
|
||||
- to execute the tests, one can simply point its browser to the route */web/tests*.
|
||||
In that case, the browser will download all assets, and QUnit will take over.
|
||||
|
||||
- there is some code in `qunit_config.js`_ which logs in the console some
|
||||
information when a test passes or fails.
|
||||
|
||||
- we want the runbot to also run these tests, so there is a test (in `test_js.py`_)
|
||||
which simply spawns a browser and points it to the *web/tests* url. Note that
|
||||
the phantom_js method does not spawn phantom_js, but Chrome headless instead.
|
||||
|
||||
|
||||
Modularity and testing
|
||||
----------------------
|
||||
|
||||
With the way Odoo is designed, any addon can modify the behaviour of other parts
|
||||
of the system. For example, the *voip* addon can modify the *FieldPhone* widget
|
||||
to use extra features. This is not really good from the perspective of the
|
||||
testing system, since this means that a test in the addon web will fail whenever
|
||||
the voip addon is installed (note that the runbot runs the tests with all addons
|
||||
installed).
|
||||
|
||||
At the same time, our testing sytem is good, because it can detect whenever
|
||||
another module breaks some core functionality. There is no complete solution to
|
||||
this issue. For now, we solve this on a case by case basis.
|
||||
|
||||
Usually, it is not a good idea to modify some other behaviour. For our voip
|
||||
example, it is certainly cleaner to add a new *FieldVOIPPhone* widget and
|
||||
modify the few views that needs it. This way, the *FieldPhone* widget is not
|
||||
impacted, and both can be tested.
|
||||
|
||||
Adding a new test case
|
||||
----------------------
|
||||
|
||||
Let us assume that we are maintaining an addon *my_addon*, and that we
|
||||
want to add a test for some javascript code (for example, some utility function
|
||||
myFunction, located in *my_addon.utils*). The process to add a new test case is
|
||||
the following:
|
||||
|
||||
1. create a new file *my_addon/static/tests/utils_tests.js*. This file contains the basic code to
|
||||
add a QUnit module *my_addon > utils*.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
odoo.define('my_addon.utils_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var utils = require('my_addon.utils');
|
||||
|
||||
QUnit.module('my_addon', {}, function () {
|
||||
|
||||
QUnit.module('utils');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
2. In *my_addon/assets.xml*, add the file to the main test assets:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="qunit_suite" name="my addon tests" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/my_addon/static/tests/utils_tests.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
3. Restart the server and update *my_addon*, or do it from the interface (to
|
||||
make sure the new test file is loaded)
|
||||
|
||||
4. Add a test case after the definition of the *utils* sub test suite:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
QUnit.test("some test case that we want to test", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var result = utils.myFunction(someArgument);
|
||||
assert.strictEqual(result, expectedResult);
|
||||
});
|
||||
|
||||
5. Visit */web/tests/* to make sure the test is executed
|
||||
|
||||
Helper functions and specialized assertions
|
||||
-------------------------------------------
|
||||
|
||||
Without help, it is quite difficult to test some parts of Odoo. In particular,
|
||||
views are tricky, because they communicate with the server and may perform many
|
||||
rpcs, which needs to be mocked. This is why we developed some specialized
|
||||
helper functions, located in `test_utils.js`_.
|
||||
|
||||
- Mock test functions: these functions help setting up a test environment. The
|
||||
most important use case is mocking the answers given by the Odoo server. These
|
||||
functions use a `mock server`_. This is a javascript class that simulates
|
||||
answers to the most common model methods: read, search_read, nameget, ...
|
||||
|
||||
- DOM helpers: useful to simulate events/actions on some specific target. For
|
||||
example, testUtils.dom.click performs a click on a target. Note that it is
|
||||
safer than doing it manually, because it also checks that the target exists,
|
||||
and is visible.
|
||||
|
||||
- create helpers: they are probably the most important functions exported by
|
||||
`test_utils.js`_. These helpers are useful to create a widget, with a mock
|
||||
environment, and a lot of small detail to simulate as much as possible the
|
||||
real conditions. The most important is certainly `createView`_.
|
||||
|
||||
- `qunit assertions`_: QUnit can be extended with specialized assertions. For
|
||||
Odoo, we frequently test some DOM properties. This is why we made some
|
||||
assertions to help with that. For example, the *containsOnce* assertion takes
|
||||
a widget/jQuery/HtmlElement and a selector, then checks if the target contains
|
||||
exactly one match for the css selector.
|
||||
|
||||
For example, with these helpers, here is what a simple form test could look like:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
QUnit.test('simple group rendering', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = testUtils.createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'</group>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(form, 'table.o_inner_group');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
Notice the use of the testUtils.createView helper and of the containsOnce
|
||||
assertion. Also, the form controller was properly destroyed at the end of
|
||||
the test.
|
||||
|
||||
Best Practices
|
||||
--------------
|
||||
|
||||
In no particular order:
|
||||
|
||||
- all test files should be added in *some_addon/static/tests/*
|
||||
- for bug fixes, make sure that the test fails without the bug fix, and passes
|
||||
with it. This ensures that it actually works.
|
||||
- try to have the minimal amount of code necessary for the test to work.
|
||||
- usually, two small tests are better than one large test. A smaller test is
|
||||
easier to understand and to fix.
|
||||
- always cleanup after a test. For example, if your test instantiates a widget,
|
||||
it should destroy it at the end.
|
||||
- no need to have full and complete code coverage. But adding a few tests helps
|
||||
a lot: it makes sure that your code is not completely broken, and whenever a
|
||||
bug is fixed, it is really much easier to add a test to an existing test suite.
|
||||
- if you want to check some negative assertion (for example, that a HtmlElement
|
||||
does not have a specific css class), then try to add the positive assertion in
|
||||
the same test (for example, by doing an action that changes the state). This
|
||||
will help avoid the test to become dead in the future (for example, if the css
|
||||
class is changed).
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
- running only one test: you can (temporarily!) change the *QUnit.test(...)*
|
||||
definition into *QUnit.only(...)*. This is useful to make sure that QUnit
|
||||
only runs this specific test.
|
||||
- debug flag: most create utility functions have a debug mode (activated by the
|
||||
debug: true parameter). In that case, the target widget will be put in the DOM
|
||||
instead of the hidden qunit specific fixture, and more information will be
|
||||
logged. For example, all mocked network communications will be available in the
|
||||
console.
|
||||
- when working on a failing test, it is common to add the debug flag, then
|
||||
comment the end of the test (in particular, the destroy call). With this, it
|
||||
is possible to see the state of the widget directly, and even better, to
|
||||
manipulate the widget by clicking/interacting with it.
|
||||
|
||||
|
||||
Integration Testing
|
||||
===================
|
||||
|
||||
Testing Python code and JS code separately is very useful, but it does not prove
|
||||
that the web client and the server work together. In order to do that, we can
|
||||
write another kind of test: tours. A tour is a mini scenario of some interesting
|
||||
business flow. It explains a sequence of steps that should be followed. The
|
||||
test runner will then create a phantom_js browser, point it to the proper url
|
||||
and simulate the click and inputs, according to the scenario.
|
||||
|
||||
Screenshots and screencasts during browser_js tests
|
||||
---------------------------------------------------
|
||||
|
||||
When running tests that use HttpCase.browser_js from the command line, the Chrome
|
||||
browser is used in headless mode. By default, if a test fails, a PNG screenshot is
|
||||
taken at the moment of the failure and written in
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
'/tmp/odoo_tests/{db_name}/screenshots/'
|
||||
|
||||
Two new command line arguments were added since Odoo 13.0 to control this behavior:
|
||||
:option:`--screenshots <odoo-bin --screenshots>` and :option:`--screencasts <odoo-bin --screencasts>`
|
||||
|
||||
|
||||
.. _qunit: https://qunitjs.com/
|
||||
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
|
||||
.. _web.js_tests_assets: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L427
|
||||
.. _web.qunit_suite: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L509
|
||||
.. _controller: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/controllers/main.py#L637
|
||||
.. _test_js.py: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/tests/test_js.py#L13
|
||||
.. _test_utils.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils.js
|
||||
.. _mock server: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/mock_server.js
|
||||
.. _qunit assertions: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_asserts.js
|
||||
.. _createView: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils_create.js#L267
|
||||
@@ -0,0 +1,212 @@
|
||||
|
||||
.. _reference/translations:
|
||||
|
||||
|
||||
===================
|
||||
Translating Modules
|
||||
===================
|
||||
|
||||
This section explains how to provide translation abilities to your module.
|
||||
|
||||
.. note:: If you want to contribute to the translation of Odoo itself, please refer to the
|
||||
`Odoo Wiki page <https://github.com/odoo/odoo/wiki/Translations>`_.
|
||||
|
||||
Exporting translatable term
|
||||
===========================
|
||||
|
||||
A number of terms in your modules are "implicitly translatable" as a result,
|
||||
even if you haven't done any specific work towards translation you can export
|
||||
your module's translatable terms and may find content to work with.
|
||||
|
||||
.. todo:: needs technical features
|
||||
|
||||
Translations export is performed via the administration interface by logging into
|
||||
the backend interface and opening :menuselection:`Settings --> Translations
|
||||
--> Import / Export --> Export Translations`
|
||||
|
||||
* leave the language to the default (new language/empty template)
|
||||
* select the `PO File`_ format
|
||||
* select your module
|
||||
* click :guilabel:`Export` and download the file
|
||||
|
||||
.. image:: translations/po-export.*
|
||||
:align: center
|
||||
:width: 75%
|
||||
|
||||
This gives you a file called :file:`{yourmodule}.pot` which should be moved to
|
||||
the :file:`{yourmodule}/i18n/` directory. The file is a *PO Template* which
|
||||
simply lists translatable strings and from which actual translations (PO files)
|
||||
can be created. PO files can be created using msginit_, with a dedicated
|
||||
translation tool like POEdit_ or by simply copying the template to a new file
|
||||
called :file:`{language}.po`. Translation files should be put in
|
||||
:file:`{yourmodule}/i18n/`, next to :file:`{yourmodule}.pot`, and will be
|
||||
automatically loaded by Odoo when the corresponding language is installed (via
|
||||
:menuselection:`Settings --> Translations --> Languages`)
|
||||
|
||||
.. note:: translations for all loaded languages are also installed or updated
|
||||
when installing or updating a module
|
||||
|
||||
Implicit exports
|
||||
================
|
||||
|
||||
Odoo automatically exports translatable strings from "data"-type content:
|
||||
|
||||
* in non-QWeb views, all text nodes are exported as well as the content of
|
||||
the ``string``, ``help``, ``sum``, ``confirm`` and ``placeholder``
|
||||
attributes
|
||||
* QWeb templates (both server-side and client-side), all text nodes are
|
||||
exported except inside ``t-translation="off"`` blocks, the content of the
|
||||
``title``, ``alt``, ``label`` and ``placeholder`` attributes are also
|
||||
exported
|
||||
* for :class:`~odoo.fields.Field`, unless their model is marked with
|
||||
``_translate = False``:
|
||||
|
||||
* their ``string`` and ``help`` attributes are exported
|
||||
* if ``selection`` is present and a list (or tuple), it's exported
|
||||
* if their ``translate`` attribute is set to ``True``, all of their existing
|
||||
values (across all records) are exported
|
||||
* help/error messages of :attr:`~odoo.models.Model._constraints` and
|
||||
:attr:`~odoo.models.Model._sql_constraints` are exported
|
||||
|
||||
Explicit exports
|
||||
================
|
||||
|
||||
When it comes to more "imperative" situations in Python code or Javascript
|
||||
code, Odoo cannot automatically export translatable terms so they
|
||||
must be marked explicitly for export. This is done by wrapping a literal
|
||||
string in a function call.
|
||||
|
||||
In Python, the wrapping function is :func:`odoo._`::
|
||||
|
||||
title = _("Bank Accounts")
|
||||
|
||||
In JavaScript, the wrapping function is generally :js:func:`odoo.web._t`:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
title = _t("Bank Accounts");
|
||||
|
||||
.. warning::
|
||||
|
||||
Only literal strings can be marked for exports, not expressions or
|
||||
variables. For situations where strings are formatted, this means the
|
||||
format string must be marked, not the formatted string
|
||||
|
||||
The lazy version of `_` and `_t` is :func:`odoo._lt` in python and
|
||||
:js:func:`odoo.web._lt` in javascript. The translation lookup is executed only
|
||||
at rendering and can be used to declare translatable properties in class methods
|
||||
of global variables.
|
||||
|
||||
Variables
|
||||
^^^^^^^^^
|
||||
**Don't** the extract may work but it will not translate the text correctly::
|
||||
|
||||
_("Scheduled meeting with %s" % invitee.name)
|
||||
|
||||
**Do** set the dynamic variables outside of the translation lookup::
|
||||
|
||||
_("Scheduled meeting with %s") % invitee.name
|
||||
|
||||
|
||||
Blocks
|
||||
^^^^^^
|
||||
**Don't** split your translation in several blocks or multiples lines::
|
||||
|
||||
# bad, trailing spaces, blocks out of context
|
||||
_("You have ") + len(invoices) + _(" invoices waiting")
|
||||
_t("You have ") + invoices.length + _t(" invoices waiting");
|
||||
|
||||
# bad, multiple small translations
|
||||
_("Reference of the document that generated ") + \
|
||||
_("this sales order request.")
|
||||
|
||||
**Do** keep in one block, giving the full context to translators::
|
||||
|
||||
# good, allow to change position of the number in the translation
|
||||
_("You have %s invoices wainting") % len(invoices)
|
||||
_.str.sprintf(_t("You have %s invoices wainting"), invoices.length);
|
||||
|
||||
# good, full sentence is understandable
|
||||
_("Reference of the document that generated " + \
|
||||
"this sales order request.")
|
||||
|
||||
Plural
|
||||
^^^^^^
|
||||
**Don't** pluralize terms the English-way::
|
||||
|
||||
msg = _("You have %s invoice") % invoice_count
|
||||
if invoice_count > 1:
|
||||
msg += _("s")
|
||||
|
||||
**Do** keep in mind every language has different plural forms::
|
||||
|
||||
if invoice_count > 1:
|
||||
msg = _("You have %s invoices") % invoice_count
|
||||
else:
|
||||
msg = _("You have %s invoice") % invoice_count
|
||||
|
||||
Read vs Run Time
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
**Don't** invoke translation lookup at server launch::
|
||||
|
||||
ERROR_MESSAGE = {
|
||||
# bad, evaluated at server launch with no user language
|
||||
'access_error': _('Access Error'),
|
||||
'missing_error': _('Missing Record'),
|
||||
}
|
||||
|
||||
class Record(models.Model):
|
||||
|
||||
def _raise_error(self, code):
|
||||
raise UserError(ERROR_MESSAGE[code])
|
||||
|
||||
**Don't** invoke translation lookup when the javascript file is read::
|
||||
|
||||
# bad, js _t is evaluated too early
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var map_title = {
|
||||
access_error: _t('Access Error'),
|
||||
missing_error: _t('Missing Record'),
|
||||
};
|
||||
|
||||
|
||||
**Do** use lazy translation lookup method::
|
||||
|
||||
ERROR_MESSAGE = {
|
||||
'access_error': _lt('Access Error'),
|
||||
'missing_error': _lt('Missing Record'),
|
||||
}
|
||||
|
||||
class Record(models.Model):
|
||||
|
||||
def _raise_error(self, code):
|
||||
# translation lookup executed at error rendering
|
||||
raise UserError(ERROR_MESSAGE[code])
|
||||
|
||||
|
||||
or **do** evaluate dynamically the translatable content::
|
||||
|
||||
# good, evaluated at run time
|
||||
def _get_error_message(self):
|
||||
return {
|
||||
access_error: _('Access Error'),
|
||||
missing_error: _('Missing Record'),
|
||||
}
|
||||
|
||||
**Do** in the case where the translation lookup is done when the JS file is
|
||||
*read*, use `_lt` instead of `_t` to translate the term when it is *used*::
|
||||
|
||||
# good, js _lt is evaluated lazily
|
||||
var core = require('web.core');
|
||||
var _lt = core._lt;
|
||||
var map_title = {
|
||||
access_error: _lt('Access Error'),
|
||||
missing_error: _lt('Missing Record'),
|
||||
};
|
||||
|
||||
|
||||
.. _PO File: https://en.wikipedia.org/wiki/Gettext#Translating
|
||||
.. _msginit: https://www.gnu.org/software/gettext/manual/gettext.html#Creating
|
||||
.. _POEdit: https://poedit.net/
|
||||
|
After Width: | Height: | Size: 11 KiB |