[MERGE] Forward-port of branch 12.0 to 13.0

This commit is contained in:
Antoine Vandevenne (anv)
2021-05-04 16:31:06 +02:00
3332 changed files with 40884 additions and 59758 deletions
+444
View File
@@ -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)
+704
View File
@@ -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``.
+316
View File
@@ -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
File diff suppressed because it is too large Load Diff
+86
View File
@@ -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:
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+139
View File
@@ -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.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+211
View File
@@ -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
+147
View File
@@ -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
File diff suppressed because it is too large Load Diff
+685
View File
@@ -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 ``&lt;li&gt;ok&lt;/li&gt;`` (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
+353
View File
@@ -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="{&quot;no_marker&quot;: 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&amp;value=%s&amp;width=%s&amp;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
+388
View File
@@ -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
'&lt;R&amp;D&gt;'
# 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>&lt;R&amp;D&gt;</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"
1 id country_id:id name code
2 state_au_1 au Australian Capital Territory ACT
3 state_au_2 au New South Wales NSW
4 state_au_3 au Northern Territory NT
5 state_au_4 au Queensland QLD
6 state_au_5 au South Australia SA
7 state_au_6 au Tasmania TAS
8 state_au_7 au Victoria VIC
9 state_au_8 au Western Australia WA
10 state_us_1 us Alabama AL
11 state_us_2 us Alaska AK
12 state_us_3 us Arizona AZ
13 state_us_4 us Arkansas AR
14 state_us_5 us California CA
15 state_us_6 us Colorado CO
+530
View File
@@ -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/
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load Diff